Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Analytics #606

Merged
merged 14 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/lazy-coins-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@khanacademy/math-input": major
"@khanacademy/perseus": major
"@khanacademy/perseus-core": minor
---

Rename analytics prop from onEvent to onAnalyticsEvent
5 changes: 5 additions & 0 deletions .changeset/poor-gifts-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": major
---

Remove 'analytics' key from PerseusDependencies
2 changes: 2 additions & 0 deletions .changeset/sharp-mice-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
5 changes: 5 additions & 0 deletions .changeset/small-snails-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/perseus": minor
---

Introduce `dependencies` on ArticleRenderer, ItemRenderer, MultiRenderer, and ServerItemRenderer.
5 changes: 5 additions & 0 deletions .changeset/thin-islands-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/math-input": minor
---

Introduce `dependencies` on Keypad.
11 changes: 9 additions & 2 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import * as React from "react";

Check warning on line 1 in .storybook/preview.js

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and Test (ubuntu-latest, 16.x)

File ignored by default.
import Color from "@khanacademy/wonder-blocks-color";
import {RenderStateRoot} from "@khanacademy/wonder-blocks-core";
import {Dependencies} from "@khanacademy/perseus";
import {storybookTestDependencies} from "../testing/test-dependencies";

import {DependenciesContext} from "../packages/perseus/src/dependencies";
import {
storybookTestDependencies,
storybookDependenciesV2,
} from "../testing/test-dependencies";

// IMPORTANT: This code runs ONCE per story file, not per story within that file.
// If you want code to run once per story, see `StorybookWrapper`.
Expand All @@ -14,7 +19,9 @@
export const decorators = [
(Story) => (
<RenderStateRoot>
<Story />
<DependenciesContext.Provider value={storybookDependenciesV2}>
<Story />
</DependenciesContext.Provider>
</RenderStateRoot>
),
];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {PerseusAnalyticsEvent} from "@khanacademy/perseus-core";
import {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core";
import Color from "@khanacademy/wonder-blocks-color";
import {Popover} from "@khanacademy/wonder-blocks-popover";
import {render, screen} from "@testing-library/react";
Expand All @@ -16,13 +16,13 @@ import Keypad from "../index";
type Props = {
onChangeMathInput: (mathInputTex: string) => void;
keypadClosed?: boolean;
sendEvent?: (event: PerseusAnalyticsEvent) => Promise<void>;
onAnalyticsEvent?: AnalyticsEventHandlerFn;
};

function V2KeypadWithMathquill(props: Props) {
const mathFieldWrapperRef = React.useRef<HTMLDivElement>(null);
const [mathField, setMathField] = React.useState<MathFieldInterface>();
const {onChangeMathInput, keypadClosed, sendEvent} = props;
const {onChangeMathInput, keypadClosed, onAnalyticsEvent} = props;
const [keypadOpen, setKeypadOpen] = React.useState<boolean>(!keypadClosed);

React.useEffect(() => {
Expand Down Expand Up @@ -74,7 +74,11 @@ function V2KeypadWithMathquill(props: Props) {
multiplicationDot
preAlgebra
trigonometry
sendEvent={sendEvent ? sendEvent : async () => {}}
onAnalyticsEvent={
onAnalyticsEvent
? onAnalyticsEvent
: async () => {}
}
showDismiss
/>
</div>
Expand Down Expand Up @@ -251,20 +255,20 @@ describe("Keypad v2 with MathQuill", () => {
// Keypad event tests
it("fires the keypad open event on open", () => {
// Arrange
const mockSendEvent = jest.fn();
const mockAnalyticsEventHandler = jest.fn();
render(
<V2KeypadWithMathquill
onChangeMathInput={() => {}}
keypadClosed={true}
sendEvent={mockSendEvent}
onAnalyticsEvent={mockAnalyticsEventHandler}
/>,
);

// Act
userEvent.click(screen.getByRole("button", {name: "Keypad toggle"}));

// Assert
expect(mockSendEvent).toHaveBeenLastCalledWith({
expect(mockAnalyticsEventHandler).toHaveBeenLastCalledWith({
type: "math-input:keypad-opened",
payload: {virtualKeypadVersion: "MATH_INPUT_KEYPAD_V2"},
});
Expand All @@ -273,19 +277,19 @@ describe("Keypad v2 with MathQuill", () => {
// Keypad event tests
it("fires the keypad open event on close", () => {
// Arrange
const mockSendEvent = jest.fn();
const mockAnalyticsEventHandler = jest.fn();
render(
<V2KeypadWithMathquill
onChangeMathInput={() => {}}
sendEvent={mockSendEvent}
onAnalyticsEvent={mockAnalyticsEventHandler}
/>,
);

// Act
userEvent.click(screen.getByRole("button", {name: "Keypad toggle"}));

// Assert
expect(mockSendEvent).toHaveBeenLastCalledWith({
expect(mockAnalyticsEventHandler).toHaveBeenLastCalledWith({
type: "math-input:keypad-closed",
payload: {virtualKeypadVersion: "MATH_INPUT_KEYPAD_V2"},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("keypad", () => {
cursorContext={
context as typeof CursorContext[keyof typeof CursorContext]
}
sendEvent={async () => {
onAnalyticsEvent={async () => {
/* TODO: verify correct analytics event sent */
}}
/>,
Expand All @@ -49,7 +49,7 @@ describe("keypad", () => {
render(
<Keypad
onClickKey={() => {}}
sendEvent={async () => {}}
onAnalyticsEvent={async () => {}}
showDismiss
/>,
);
Expand All @@ -65,7 +65,9 @@ describe("keypad", () => {
it(`hides optional dismiss button`, () => {
// Arrange
// Act
render(<Keypad onClickKey={() => {}} sendEvent={async () => {}} />);
render(
<Keypad onClickKey={() => {}} onAnalyticsEvent={async () => {}} />,
);

// Assert
expect(
Expand Down
12 changes: 6 additions & 6 deletions packages/math-input/src/components/keypad/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import NumbersPage from "./keypad-pages/numbers-page";
import OperatorsPage from "./keypad-pages/operators-page";
import SharedKeys from "./shared-keys";

import type {SendEventFn} from "@khanacademy/perseus-core";
import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core";

export type Props = {
extraKeys: ReadonlyArray<Key>;
Expand All @@ -33,7 +33,7 @@ export type Props = {
advancedRelations?: boolean;

onClickKey: ClickKeyCallback;
sendEvent: SendEventFn;
onAnalyticsEvent: AnalyticsEventHandlerFn;
};

const defaultProps = {
Expand Down Expand Up @@ -84,27 +84,27 @@ export default function Keypad(props: Props) {
basicRelations,
advancedRelations,
showDismiss,
sendEvent,
onAnalyticsEvent,
} = props;

useEffect(() => {
if (!isMounted) {
sendEvent({
onAnalyticsEvent({
type: "math-input:keypad-opened",
payload: {virtualKeypadVersion: "MATH_INPUT_KEYPAD_V2"},
});
setIsMounted(true);
}
return () => {
if (isMounted) {
sendEvent({
onAnalyticsEvent({
type: "math-input:keypad-closed",
payload: {virtualKeypadVersion: "MATH_INPUT_KEYPAD_V2"},
});
setIsMounted(false);
}
};
}, [sendEvent, isMounted]);
}, [onAnalyticsEvent, isMounted]);

return (
<View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function V2KeypadWithMathquill() {
multiplicationDot
preAlgebra
trigonometry
sendEvent={async (event) => {
onAnalyticsEvent={async (event) => {
// eslint-disable-next-line no-console
console.log("Send Event:", event);
}}
Expand Down
6 changes: 4 additions & 2 deletions packages/perseus-core/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@ export type PerseusAnalyticsEvent =
// key and a payload that varies by type.
// | {type: "b"; payload: {name: string}};

/** A function to send analytics events. */
export type SendEventFn = (event: PerseusAnalyticsEvent) => Promise<void>;
/** A function that is called when Perseus emits an analytics event. */
export type AnalyticsEventHandlerFn = (
event: PerseusAnalyticsEvent,
) => Promise<void>;
2 changes: 1 addition & 1 deletion packages/perseus-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type {PerseusAnalyticsEvent, SendEventFn} from "./analytics";
export type {PerseusAnalyticsEvent, AnalyticsEventHandlerFn} from "./analytics";
6 changes: 6 additions & 0 deletions packages/perseus-editor/src/multirenderer-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,12 @@ class MultiRendererEditor extends React.Component<MultiRendererEditorProps> {
item={item}
shape={itemShape}
apiOptions={apiOptions}
// Today, with analytics being the only thing in
// dependencies, we send in a dummy function as we don't
// want to gather analytics events from within the editor.
dependencies={{
analytics: {onAnalyticsEvent: async () => {}},
}}
>
{({renderers}) => (
<NodeContainer
Expand Down
36 changes: 36 additions & 0 deletions packages/perseus/src/__stories__/article-renderer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from "react";

import {storybookDependenciesV2} from "../../../../testing/test-dependencies";
import {
singleSectionArticle,
multiSectionArticle,
} from "../__testdata__/article-renderer.testdata";
import ArticleRenderer from "../article-renderer";

type StoryArgs = Record<any, any>;

type Story = {
title: string;
};

export default {
title: "Perseus/Renderers/Article Renderer",
} as Story;

export const ASingleSectionArticle = (args: StoryArgs): React.ReactElement => {
return (
<ArticleRenderer
json={singleSectionArticle}
dependencies={storybookDependenciesV2}
/>
);
};

export const BMultiSectionArticle = (args: StoryArgs): React.ReactElement => {
return (
<ArticleRenderer
json={multiSectionArticle}
dependencies={storybookDependenciesV2}
/>
);
};
72 changes: 72 additions & 0 deletions packages/perseus/src/__testdata__/article-renderer.testdata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type {PerseusRenderer} from "../perseus-types";

export const singleSectionArticle: PerseusRenderer = {
content:
"[[☃ image 2]]\n\n**Part A:** Return to the main characters from your three favorite films. \n\n- What was one important choice they had to make where the stakes were **high**?\n\n- What were the **stakes**?\n\n- Can you identify them as internal, external or philosophical?\n\n**Part B:** Think about a difficult choice you had to make in your own life. What was at stake? \n\n**Part C:** Return to one of the obstacles your character might face from the previous exercise. Now think of the **choice** this obstacle forces them to make. Answer the following:\n\n- What are the possible stakes of this choice?\n\n- Can you come up with an internal, external or philosophical stake which applies to this choice? \n",
images: {},
widgets: {
"image 2": {
type: "image",
alignment: "block",
static: false,
graded: true,
options: {
static: false,
title: "",
range: [
[0, 10],
[0, 10],
],
box: [1258, 703],
backgroundImage: {
url: "https://ka-perseus-images.s3.amazonaws.com/b08029bc1786fbe54468a2ddc96aaa20be7a663a.png",
width: 1258,
height: 703,
},
labels: [],
alt: 'A scene from Pixar\'s film "Toy Story 3" where the characters are swimming in a sea of trash and look very afraid.',
caption: "",
},
version: {major: 0, minor: 0},
},
},
};

export const multiSectionArticle: ReadonlyArray<PerseusRenderer> = [
{
content:
"## What is a matrix?\n\nA matrix is a rectangular array of numbers arranged in rows and columns.\n\nMatrices can be useful for organizing and manipulating data. They can also be used as a tool to help solve systems of equations.",
images: {},
widgets: {},
},
{
content:
"## How do we multiply a matrix by a scalar?\n\nTo multiply a matrix by a scalar, we multiply each entry in the matrix by the scalar. For example, if \n\n$A = \\begin{bmatrix} \n2 & 3 \\\\\n1 & 4 \\\\\n\\end{bmatrix}$\n\nand we want to multiply $A$ by $2$, we can multiply each entry by $2$ to get:\n\n$2A = \\begin{bmatrix} \n4 & 6 \\\\\n2 & 8 \\\\\n\\end{bmatrix}$",
images: {},
widgets: {},
},
{
content:
"## How do we add or subtract two matrices?\n\nTo add or subtract two matrices, we add or subtract corresponding entries. So if \n\n$A = \\begin{bmatrix} \n2 & 3 \\\\\n1 & 4 \\\\\n\\end{bmatrix}$ \n\nand \n\n$B = \\begin{bmatrix} \n5 & 2 \\\\\n3 & 1 \\\\\n\\end{bmatrix}$,\n\nthen \n\n$A+B = \\begin{bmatrix} \n7 & 5 \\\\\n4 & 5 \\\\\n\\end{bmatrix}$\n\nand \n\n$A-B = \\begin{bmatrix} \n-3 & 1 \\\\\n-2 & 3 \\\\\n\\end{bmatrix}$",
images: {},
widgets: {},
},
{
content:
"## How do we multiply two matrices together?\n\nThis one's a bit trickier. To multiply two matrices together, we take the dot product of the rows from the first matrix with the columns from the second matrix. The result is a new matrix. For example, if \n\n$A = \\begin{bmatrix} \n2 & 3 \\\\\n1 & 4 \\\\\n\\end{bmatrix}$ \n\nand \n\n$B = \\begin{bmatrix} \n5 & 2 \\\\\n3 & 1 \\\\\n\\end{bmatrix}$,\n\nthen \n\n$AB = \\begin{bmatrix} \n2\\cdot5+3\\cdot3 & 2\\cdot2+3\\cdot1 \\\\\n1\\cdot5+4\\cdot3 & 1\\cdot2+4\\cdot1 \\\\\n\\end{bmatrix} = \\begin{bmatrix} \n19 & 7 \\\\\n17 & 6 \\\\\n\\end{bmatrix}.$",
images: {},
widgets: {},
},
{
content:
"## What can we do with the inverse of a matrix?\n\nFinding the inverse of a matrix can be useful for solving linear systems of equations. If $A$ is the matrix representing the system of equations, and $b$ is the vector of solutions, then $Ax=b$. If we can find the inverse of $A$, we can multiply both sides of the equation by it to isolate $x$:\n\n$A^{-1}Ax=A^{-1}b \\Rightarrow x=A^{-1}b.$",
images: {},
widgets: {},
},
{
content:
"## Where are matrices used in the real world?\n\nMatrices have tons of real-world applications! They can be used in computer graphics to perform transformations on images, in physics to model physical systems, and in statistics to analyze data, just to name a few.",
images: {},
widgets: {},
},
];
6 changes: 5 additions & 1 deletion packages/perseus/src/__tests__/item-renderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import userEvent from "@testing-library/user-event";
import * as React from "react";
import "@testing-library/jest-dom"; // Imports custom matchers

import {testDependencies} from "../../../../testing/test-dependencies";
import {
testDependencies,
testDependenciesV2,
} from "../../../../testing/test-dependencies";
import {
itemWithInput,
labelImageItem,
Expand Down Expand Up @@ -76,6 +79,7 @@ export const renderQuestion = (
reviewMode={false}
savedState=""
controlPeripherals={false}
dependencies={testDependenciesV2}
{...optionalProps}
/>
{/* The ItemRenderer _requires_ two divs: a work area and hints
Expand Down
Loading
Loading