Skip to content

Commit 34a0db0

Browse files
author
Llorenç Muntaner
authored
Add properties to Funnel (#2982)
* Add properties to Funnel * Format
1 parent bb5f61f commit 34a0db0

2 files changed

Lines changed: 138 additions & 15 deletions

File tree

src/frontend/src/utils/analytics/Funnel.test.ts

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ describe("Funnel", () => {
4242

4343
it("init() - triggers start-login event", () => {
4444
funnel.init();
45-
expect(analytics.event).toHaveBeenCalledWith("start-login");
45+
expect(analytics.event).toHaveBeenCalledWith("start-login", undefined);
46+
});
47+
48+
it("init() - triggers start-login event with properties", () => {
49+
const properties = { userId: "123", source: "test" };
50+
funnel.init(properties);
51+
expect(analytics.event).toHaveBeenCalledWith("start-login", properties);
4652
});
4753

4854
it("init() - tracks window session enter when window becomes visible", () => {
@@ -51,6 +57,18 @@ describe("Funnel", () => {
5157
document.dispatchEvent(new Event("visibilitychange"));
5258
expect(analytics.event).toHaveBeenCalledWith(
5359
"start-login-window-session-enter",
60+
undefined,
61+
);
62+
});
63+
64+
it("init() - tracks window session enter with properties when window becomes visible", () => {
65+
const properties = { userId: "123", source: "test" };
66+
funnel.init(properties);
67+
mockVisibilityState("visible");
68+
document.dispatchEvent(new Event("visibilitychange"));
69+
expect(analytics.event).toHaveBeenCalledWith(
70+
"start-login-window-session-enter",
71+
properties,
5472
);
5573
});
5674

@@ -60,45 +78,108 @@ describe("Funnel", () => {
6078
document.dispatchEvent(new Event("visibilitychange"));
6179
expect(analytics.event).toHaveBeenCalledWith(
6280
"start-login-window-session-leave",
81+
undefined,
82+
);
83+
});
84+
85+
it("init() - tracks window session leave with properties when window is hidden", () => {
86+
const properties = { userId: "123", source: "test" };
87+
funnel.init(properties);
88+
mockVisibilityState("hidden");
89+
document.dispatchEvent(new Event("visibilitychange"));
90+
expect(analytics.event).toHaveBeenCalledWith(
91+
"start-login-window-session-leave",
92+
properties,
6393
);
6494
});
6595

6696
it("trigger() - tracks new registration start event", () => {
6797
funnel.trigger(LoginEvents.NewRegistrationStart);
6898
expect(analytics.event).toHaveBeenCalledWith(
6999
"login-new-registration-start",
100+
undefined,
101+
);
102+
});
103+
104+
it("trigger() - tracks new registration start event with init properties", () => {
105+
const properties = { userId: "123", source: "test" };
106+
funnel.init(properties);
107+
funnel.trigger(LoginEvents.NewRegistrationStart);
108+
expect(analytics.event).toHaveBeenCalledWith(
109+
"login-new-registration-start",
110+
properties,
111+
);
112+
});
113+
114+
it("trigger() - tracks new registration start event with additional properties", () => {
115+
const additionalProps = { step: 1, method: "email" };
116+
funnel.trigger(LoginEvents.NewRegistrationStart, additionalProps);
117+
expect(analytics.event).toHaveBeenCalledWith(
118+
"login-new-registration-start",
119+
additionalProps,
120+
);
121+
});
122+
123+
it("trigger() - merges init properties with additional properties", () => {
124+
const initProps = { userId: "123", source: "test" };
125+
const additionalProps = { step: 1, method: "email" };
126+
const expectedProps = { ...initProps, ...additionalProps };
127+
128+
funnel.init(initProps);
129+
funnel.trigger(LoginEvents.NewRegistrationStart, additionalProps);
130+
131+
expect(analytics.event).toHaveBeenCalledWith(
132+
"login-new-registration-start",
133+
expectedProps,
70134
);
71135
});
72136

73137
it("trigger() - tracks complete passkey login flow", () => {
74138
funnel.trigger(LoginEvents.ExistingUserStart);
75-
expect(analytics.event).toHaveBeenCalledWith("login-existing-user-start");
139+
expect(analytics.event).toHaveBeenCalledWith(
140+
"login-existing-user-start",
141+
undefined,
142+
);
76143

77144
funnel.trigger(LoginEvents.ExistingUserPasskey);
78-
expect(analytics.event).toHaveBeenCalledWith("login-existing-user-passkey");
145+
expect(analytics.event).toHaveBeenCalledWith(
146+
"login-existing-user-passkey",
147+
undefined,
148+
);
79149

80150
funnel.trigger(LoginEvents.ExistingUserPasskeySuccess);
81151
expect(analytics.event).toHaveBeenCalledWith(
82152
"login-existing-user-passkey-success",
153+
undefined,
83154
);
84155
});
85156

86157
it("trigger() - tracks complete OpenID login flow", () => {
87158
funnel.trigger(LoginEvents.ExistingUserStart);
88-
expect(analytics.event).toHaveBeenCalledWith("login-existing-user-start");
159+
expect(analytics.event).toHaveBeenCalledWith(
160+
"login-existing-user-start",
161+
undefined,
162+
);
89163

90164
funnel.trigger(LoginEvents.ExistingUserOpenId);
91-
expect(analytics.event).toHaveBeenCalledWith("login-existing-user-openid");
165+
expect(analytics.event).toHaveBeenCalledWith(
166+
"login-existing-user-openid",
167+
undefined,
168+
);
92169

93170
funnel.trigger(LoginEvents.ExistingUserOpenIdSuccess);
94171
expect(analytics.event).toHaveBeenCalledWith(
95172
"login-existing-user-openid-success",
173+
undefined,
96174
);
97175
});
98176

99177
it("trigger() - tracks recovery start event", () => {
100178
funnel.trigger(LoginEvents.RecoveryStart);
101-
expect(analytics.event).toHaveBeenCalledWith("login-recovery-start");
179+
expect(analytics.event).toHaveBeenCalledWith(
180+
"login-recovery-start",
181+
undefined,
182+
);
102183
});
103184

104185
it("close() - stops tracking window session events", () => {
@@ -120,7 +201,27 @@ describe("Funnel", () => {
120201
vi.useFakeTimers();
121202

122203
funnel.init();
123-
expect(analytics.event).toHaveBeenCalledWith("start-login");
204+
expect(analytics.event).toHaveBeenCalledWith("start-login", undefined);
205+
expect(analytics.event).toHaveBeenCalledTimes(1);
206+
207+
// Advance time by 5 seconds
208+
vi.advanceTimersByTime(5000);
209+
210+
funnel.close();
211+
expect(analytics.event).toHaveBeenCalledTimes(2);
212+
expect(analytics.event).toHaveBeenCalledWith("end-login", {
213+
"duration-login": 5,
214+
});
215+
216+
vi.useRealTimers();
217+
});
218+
219+
it("close() - tracks duration since init and includes init properties", () => {
220+
vi.useFakeTimers();
221+
222+
const properties = { userId: "123", source: "test" };
223+
funnel.init(properties);
224+
expect(analytics.event).toHaveBeenCalledWith("start-login", properties);
124225
expect(analytics.event).toHaveBeenCalledTimes(1);
125226

126227
// Advance time by 5 seconds
@@ -129,6 +230,7 @@ describe("Funnel", () => {
129230
funnel.close();
130231
expect(analytics.event).toHaveBeenCalledTimes(2);
131232
expect(analytics.event).toHaveBeenCalledWith("end-login", {
233+
...properties,
132234
"duration-login": 5,
133235
});
134236

src/frontend/src/utils/analytics/Funnel.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,30 @@ export class Funnel<T extends Record<string, string>> {
55
#name: string;
66
#cleanupSession?: () => void;
77
#startTimestamp?: number;
8+
#properties?: Record<string, string | number>;
89

910
constructor(name: string) {
1011
this.#name = name;
1112
}
1213

13-
init(): void {
14+
init(properties?: Record<string, string | number>): void {
1415
this.#startTimestamp = Date.now();
15-
analytics.event("start-" + this.#name);
16+
this.#properties = properties;
17+
analytics.event("start-" + this.#name, this.#properties);
1618

1719
// Start window session tracking
1820
this.#cleanupSession = trackWindowSession({
1921
onEnterSession: () => {
20-
analytics.event(`start-${this.#name}-window-session-enter`);
22+
analytics.event(
23+
`start-${this.#name}-window-session-enter`,
24+
this.#properties,
25+
);
2126
},
2227
onLeaveSession: () => {
23-
analytics.event(`start-${this.#name}-window-session-leave`);
28+
analytics.event(
29+
`start-${this.#name}-window-session-leave`,
30+
this.#properties,
31+
);
2432
},
2533
});
2634
}
@@ -39,14 +47,27 @@ export class Funnel<T extends Record<string, string>> {
3947
) {
4048
const durationMs = Date.now() - this.#startTimestamp;
4149
const durationSec = durationMs / 1000;
42-
analytics.event(`end-${this.#name}`, {
50+
const eventProperties = {
51+
...(this.#properties || {}),
4352
[`duration-${this.#name}`]: durationSec,
44-
});
53+
};
54+
analytics.event(`end-${this.#name}`, eventProperties);
4555
this.#startTimestamp = undefined;
4656
}
4757
}
4858

49-
trigger(event: T[keyof T]): void {
50-
analytics.event(event);
59+
trigger(
60+
event: T[keyof T],
61+
additionalProperties?: Record<string, string | number>,
62+
): void {
63+
const eventProperties = {
64+
...(this.#properties || {}),
65+
...(additionalProperties || {}),
66+
};
67+
68+
analytics.event(
69+
event,
70+
Object.keys(eventProperties).length > 0 ? eventProperties : undefined,
71+
);
5172
}
5273
}

0 commit comments

Comments
 (0)