Skip to content

Commit 6fa388c

Browse files
[Analytics] Adding rendering telemetry events to widgets in Perseus. (#3017)
## Summary: Currently in our analytics, only interactive graph logs rendering metrics. This PR is to fix it and expose rendering data for more Perseus Widgets. Issue: LEMS-XXXX ## Test plan: Author: catandthemachines Reviewers: nishasy, catandthemachines, ivyolamit, jeremywiebe, SonicScrewdriver, handeyeco, Myranae Required Reviewers: Approved By: nishasy Checks: ✅ 10 checks were successful Pull Request URL: #3017
1 parent 86dfe96 commit 6fa388c

File tree

11 files changed

+158
-7
lines changed

11 files changed

+158
-7
lines changed

.changeset/funny-dancers-yell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/perseus": patch
3+
---
4+
5+
Adding rendering analytics events to perseus widgets

packages/perseus/src/widgets/expression/expression.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,30 @@ describe("Expression Widget", function () {
458458
);
459459
});
460460

461+
it("should send analytics event when widget is rendered", () => {
462+
// Arrange
463+
const onAnalyticsEventSpy = jest.spyOn(
464+
testDependenciesV2.analytics,
465+
"onAnalyticsEvent",
466+
);
467+
468+
// Act
469+
renderQuestion(expressionItem2.question);
470+
act(() => {
471+
jest.runAllTimers();
472+
});
473+
474+
// Assert
475+
expect(onAnalyticsEventSpy).toHaveBeenCalledWith({
476+
type: "perseus:widget:rendered:ti",
477+
payload: {
478+
widgetSubType: "null",
479+
widgetType: "expression",
480+
widgetId: "expression",
481+
},
482+
});
483+
});
484+
461485
it("supports mobile rendering", async () => {
462486
// arrange and act
463487
renderQuestion(expressionItem2.question, {

packages/perseus/src/widgets/expression/expression.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ export class Expression extends React.Component<Props> implements Widget {
103103
displayName = "Expression";
104104

105105
componentDidMount: () => void = () => {
106+
this.props.analytics?.onAnalyticsEvent({
107+
type: "perseus:widget:rendered:ti",
108+
payload: {
109+
widgetSubType: "null",
110+
widgetType: "expression",
111+
widgetId: "expression",
112+
},
113+
});
114+
106115
// TODO(scottgrant): This is a hack to remove the deprecated call to
107116
// this.isMounted() but is still considered an anti-pattern.
108117
this._isMounted = true;

packages/perseus/src/widgets/image/image.test.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ import {userEvent as userEventLib} from "@testing-library/user-event";
88

99
import {getFeatureFlags} from "../../../../../testing/feature-flags-util";
1010
import {mockImageLoading} from "../../../../../testing/image-loader-utils";
11-
import {testDependencies} from "../../../../../testing/test-dependencies";
11+
import {
12+
testDependenciesV2,
13+
testDependencies,
14+
} from "../../../../../testing/test-dependencies";
1215
import * as Dependencies from "../../dependencies";
1316
import {scorePerseusItemTesting} from "../../util/test-utils";
1417
import {renderQuestion} from "../__testutils__/renderQuestion";
1518

1619
import {question} from "./image.testdata";
1720
import {earthMoonImage} from "./utils";
1821

19-
import type {APIOptions} from "../../types";
22+
import type {APIOptions, PerseusDependenciesV2} from "../../types";
2023
import type {UserEvent} from "@testing-library/user-event";
2124

2225
describe.each([[true], [false]])("image widget - isMobile(%j)", (isMobile) => {
@@ -110,6 +113,42 @@ describe.each([[true], [false]])("image widget - isMobile(%j)", (isMobile) => {
110113
expect(screen.getByRole("figure")).toBeVisible();
111114
});
112115

116+
it("should send analytics event when widget is rendered", () => {
117+
// Arrange
118+
const imageQuestion = generateTestPerseusRenderer({
119+
content: "[[☃ image 1]]",
120+
widgets: {
121+
"image 1": generateImageWidget({
122+
options: generateImageOptions({
123+
backgroundImage: earthMoonImage,
124+
}),
125+
}),
126+
},
127+
});
128+
129+
const onAnalyticsEventSpy = jest.fn();
130+
const depsV2: PerseusDependenciesV2 = {
131+
...testDependenciesV2,
132+
analytics: {onAnalyticsEvent: onAnalyticsEventSpy},
133+
};
134+
135+
// Act
136+
renderQuestion(imageQuestion, apiOptions, undefined, undefined, depsV2);
137+
act(() => {
138+
jest.runAllTimers();
139+
});
140+
141+
// Assert
142+
expect(onAnalyticsEventSpy).toHaveBeenCalledWith({
143+
type: "perseus:widget:rendered:ti",
144+
payload: {
145+
widgetSubType: "null",
146+
widgetType: "image",
147+
widgetId: "image",
148+
},
149+
});
150+
});
151+
113152
it("should render image with alt text", () => {
114153
// Arrange
115154
const imageQuestion = generateTestPerseusRenderer({

packages/perseus/src/widgets/image/image.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {isFeatureOn} from "@khanacademy/perseus-core";
2+
import {useOnMountEffect} from "@khanacademy/wonder-blocks-core";
23
import * as React from "react";
34

45
import AssetContext from "../../asset-context";
56
import {PerseusI18nContext} from "../../components/i18n-context";
67
import SvgImage from "../../components/svg-image";
8+
import {useDependencies} from "../../dependencies";
79
import Renderer from "../../renderer";
810
import Util from "../../util";
911

@@ -30,6 +32,7 @@ export const ImageComponent = (props: ImageWidgetProps) => {
3032
} = props;
3133
const context = React.useContext(PerseusI18nContext);
3234
const imageUpgradeFF = isFeatureOn({apiOptions}, "image-widget-upgrade");
35+
const {analytics} = useDependencies();
3336

3437
const [zoomSize, setZoomSize] = React.useState<Size>([
3538
backgroundImage.width || 0,
@@ -38,6 +41,17 @@ export const ImageComponent = (props: ImageWidgetProps) => {
3841

3942
const [zoomWidth, zoomHeight] = zoomSize;
4043

44+
useOnMountEffect(() => {
45+
analytics.onAnalyticsEvent({
46+
type: "perseus:widget:rendered:ti",
47+
payload: {
48+
widgetSubType: "null",
49+
widgetType: "image",
50+
widgetId: "image",
51+
},
52+
});
53+
});
54+
4155
React.useEffect(() => {
4256
// Wait to figure out what the original size of the image is.
4357
// Use whichever is larger between the original image size and the

packages/perseus/src/widgets/interactive-graphs/mafs-graph.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ describe("MafsGraph", () => {
154154
type: "perseus:widget:rendered:ti",
155155
payload: {
156156
widgetSubType: "segment",
157-
widgetType: "INTERACTIVE_GRAPH",
157+
widgetType: "interactive-graph",
158158
widgetId: "interactive-graph",
159159
},
160160
});

packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export const MafsGraph = (props: MafsGraphProps) => {
142142
type: "perseus:widget:rendered:ti",
143143
payload: {
144144
widgetSubType: type,
145-
widgetType: "INTERACTIVE_GRAPH",
145+
widgetType: "interactive-graph",
146146
widgetId: "interactive-graph",
147147
},
148148
});

packages/perseus/src/widgets/label-image/__tests__/label-image.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,22 @@ describe("LabelImage", function () {
829829
);
830830
});
831831

832+
it("sends an analytics event when widget is rendered", async () => {
833+
// render component
834+
renderQuestion(textQuestion);
835+
836+
expect(
837+
testDependenciesV2.analytics.onAnalyticsEvent,
838+
).toHaveBeenCalledWith({
839+
type: "perseus:widget:rendered:ti",
840+
payload: {
841+
widgetSubType: "null",
842+
widgetType: "label-image",
843+
widgetId: "label-image",
844+
},
845+
});
846+
});
847+
832848
it("sends an analytics event when the toggle is interacted with", async () => {
833849
// render component
834850
renderQuestion(textQuestion);

packages/perseus/src/widgets/label-image/label-image.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,14 @@ export class LabelImage
352352
}
353353

354354
componentDidMount() {
355+
this.props.analytics?.onAnalyticsEvent({
356+
type: "perseus:widget:rendered:ti",
357+
payload: {
358+
widgetSubType: "null",
359+
widgetType: "label-image",
360+
widgetId: "label-image",
361+
},
362+
});
355363
this._mounted = true;
356364
}
357365

packages/perseus/src/widgets/video/video.test.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe("video widget", () => {
3434
expect(container).toMatchSnapshot("first mobile render");
3535
});
3636

37-
it("video widget should allow autoplay", () => {
37+
it("should allow autoplay", () => {
3838
// Arrange
3939
const apiOptions: APIOptions = {
4040
isMobile: false,
@@ -51,7 +51,33 @@ describe("video widget", () => {
5151
);
5252
});
5353

54-
it("vimeo widget should contain dnt param", () => {
54+
it("should send analytics event when widget is rendered", () => {
55+
// Arrange
56+
const apiOptions: APIOptions = {
57+
isMobile: false,
58+
};
59+
60+
const onAnalyticsEventSpy = jest.fn();
61+
const depsV2: PerseusDependenciesV2 = {
62+
...testDependenciesV2,
63+
analytics: {onAnalyticsEvent: onAnalyticsEventSpy},
64+
};
65+
66+
// Act
67+
renderQuestion(question1, apiOptions, undefined, undefined, depsV2);
68+
69+
// Assert
70+
expect(onAnalyticsEventSpy).toHaveBeenCalledWith({
71+
type: "perseus:widget:rendered:ti",
72+
payload: {
73+
widgetSubType: "null",
74+
widgetType: "video",
75+
widgetId: "video",
76+
},
77+
});
78+
});
79+
80+
it("should contain dnt param", () => {
5581
// Arrange
5682
const apiOptions: APIOptions = {
5783
isMobile: false,
@@ -67,7 +93,7 @@ describe("video widget", () => {
6793
);
6894
});
6995

70-
it("video widget should call the generateUrl dependency to set the iframe src", () => {
96+
it("should call the generateUrl dependency to set the iframe src", () => {
7197
// Arrange
7298
const dependencies: PerseusDependenciesV2 = {
7399
...testDependenciesV2,

0 commit comments

Comments
 (0)