Skip to content

Commit 6043b39

Browse files
authored
Analytics configuration is the responsibility of the host application when running in widget mode (#3089)
* Support for analytics configuration via URL parameters in widget mode Adds: - posthogApiHost - posthogApiKey - rageshakeSubmitUrl - sentryDsn - sentryEnvironment Deprecate analyticsId and use posthogUserId instead * Partial test coverage * Simplify tests * More tests * Lint * Split embedded only parameters into own section for clarity * Update docs/url-params.md * Update docs/url-params.md * Update vite.config.js
1 parent 7ca70cf commit 6043b39

15 files changed

+675
-102
lines changed

docs/url-params.md

Lines changed: 43 additions & 30 deletions
Large diffs are not rendered by default.

src/UrlParams.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,15 @@ export interface UrlParams {
105105
/**
106106
* The Posthog analytics ID. It is only available if the user has given consent for sharing telemetry in element web.
107107
*/
108-
analyticsID: string | null;
108+
posthogUserId: string | null;
109+
/**
110+
* The Posthog API host. This is only used in the embedded package of Element Call.
111+
*/
112+
posthogApiHost: string | null;
113+
/**
114+
* The Posthog API key. This is only used in the embedded package of Element Call.
115+
*/
116+
posthogApiKey: string | null;
109117
/**
110118
* Whether the app is allowed to use fallback STUN servers for ICE in case the
111119
* user's homeserver doesn't provide any.
@@ -155,6 +163,20 @@ export interface UrlParams {
155163
* If it was a Join Call button, it would be `join_existing`.
156164
*/
157165
intent: string | null;
166+
167+
/**
168+
* The rageshake submit URL. This is only used in the embedded package of Element Call.
169+
*/
170+
rageshakeSubmitUrl: string | null;
171+
172+
/**
173+
* The Sentry DSN. This is only used in the embedded package of Element Call.
174+
*/
175+
sentryDsn: string | null;
176+
/**
177+
* The Sentry environment. This is only used in the embedded package of Element Call.
178+
*/
179+
sentryEnvironment: string | null;
158180
}
159181

160182
// This is here as a stopgap, but what would be far nicer is a function that
@@ -257,7 +279,6 @@ export const getUrlParams = (
257279
lang: parser.getParam("lang"),
258280
fonts: parser.getAllParams("font"),
259281
fontScale: Number.isNaN(fontScale) ? null : fontScale,
260-
analyticsID: parser.getParam("analyticsID"),
261282
allowIceFallback: parser.getFlagParam("allowIceFallback"),
262283
perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"),
263284
skipLobby: parser.getFlagParam(
@@ -271,6 +292,13 @@ export const getUrlParams = (
271292
viaServers: !isWidget ? parser.getParam("viaServers") : null,
272293
homeserver: !isWidget ? parser.getParam("homeserver") : null,
273294
intent,
295+
posthogApiHost: parser.getParam("posthogApiHost"),
296+
posthogApiKey: parser.getParam("posthogApiKey"),
297+
posthogUserId:
298+
parser.getParam("posthogUserId") ?? parser.getParam("analyticsID"),
299+
rageshakeSubmitUrl: parser.getParam("rageshakeSubmitUrl"),
300+
sentryDsn: parser.getParam("sentryDsn"),
301+
sentryEnvironment: parser.getParam("sentryEnvironment"),
274302
};
275303
};
276304

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE in the repository root for full details.
6+
*/
7+
8+
import {
9+
expect,
10+
describe,
11+
it,
12+
vi,
13+
beforeEach,
14+
beforeAll,
15+
afterAll,
16+
} from "vitest";
17+
18+
import { PosthogAnalytics } from "./PosthogAnalytics";
19+
import { mockConfig } from "../utils/test";
20+
21+
describe("PosthogAnalytics", () => {
22+
describe("embedded package", () => {
23+
beforeAll(() => {
24+
vi.stubEnv("VITE_PACKAGE", "embedded");
25+
});
26+
27+
beforeEach(() => {
28+
mockConfig({});
29+
window.location.hash = "#";
30+
PosthogAnalytics.resetInstance();
31+
});
32+
33+
afterAll(() => {
34+
vi.unstubAllEnvs();
35+
});
36+
37+
it("does not create instance without config value or URL params", () => {
38+
expect(PosthogAnalytics.instance.isEnabled()).toBe(false);
39+
});
40+
41+
it("ignores config value and does not create instance", () => {
42+
mockConfig({
43+
posthog: {
44+
api_host: "https://api.example.com.localhost",
45+
api_key: "api_key",
46+
},
47+
});
48+
expect(PosthogAnalytics.instance.isEnabled()).toBe(false);
49+
});
50+
51+
it("uses URL params if both set", () => {
52+
window.location.hash = `#?posthogApiHost=${encodeURIComponent("https://url.example.com.localhost")}&posthogApiKey=api_key`;
53+
expect(PosthogAnalytics.instance.isEnabled()).toBe(true);
54+
});
55+
});
56+
57+
describe("full package", () => {
58+
beforeAll(() => {
59+
vi.stubEnv("VITE_PACKAGE", "full");
60+
});
61+
62+
beforeEach(() => {
63+
mockConfig({});
64+
window.location.hash = "#";
65+
PosthogAnalytics.resetInstance();
66+
});
67+
68+
afterAll(() => {
69+
vi.unstubAllEnvs();
70+
});
71+
72+
it("does not create instance without config value", () => {
73+
expect(PosthogAnalytics.instance.isEnabled()).toBe(false);
74+
});
75+
76+
it("ignores URL params and does not create instance", () => {
77+
window.location.hash = `#?posthogApiHost=${encodeURIComponent("https://url.example.com.localhost")}&posthogApiKey=api_key`;
78+
expect(PosthogAnalytics.instance.isEnabled()).toBe(false);
79+
});
80+
81+
it("creates instance with config value", () => {
82+
mockConfig({
83+
posthog: {
84+
api_host: "https://api.example.com.localhost",
85+
api_key: "api_key",
86+
},
87+
});
88+
expect(PosthogAnalytics.instance.isEnabled()).toBe(true);
89+
});
90+
});
91+
});

src/analytics/PosthogAnalytics.ts

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,6 @@ interface PlatformProperties {
7171
cryptoVersion?: string;
7272
}
7373

74-
interface PosthogSettings {
75-
project_api_key?: string;
76-
api_host?: string;
77-
}
78-
7974
export class PosthogAnalytics {
8075
/* Wrapper for Posthog analytics.
8176
* 3 modes of anonymity are supported, governed by this.anonymity
@@ -113,24 +108,27 @@ export class PosthogAnalytics {
113108
return this.internalInstance;
114109
}
115110

116-
private constructor(private readonly posthog: PostHog) {
117-
const posthogConfig: PosthogSettings = {
118-
project_api_key: Config.get().posthog?.api_key,
119-
api_host: Config.get().posthog?.api_host,
120-
};
111+
public static resetInstance(): void {
112+
// Reset the singleton instance
113+
this.internalInstance = null;
114+
}
121115

122-
if (posthogConfig.project_api_key && posthogConfig.api_host) {
123-
if (
124-
PosthogAnalytics.getPlatformProperties().matrixBackend === "embedded"
125-
) {
126-
const { analyticsID } = getUrlParams();
127-
// if the embedding platform (element web) already got approval to communicating with posthog
128-
// element call can also send events to posthog
129-
optInAnalytics.setValue(Boolean(analyticsID));
130-
}
116+
private constructor(private readonly posthog: PostHog) {
117+
let apiKey: string | undefined;
118+
let apiHost: string | undefined;
119+
if (import.meta.env.VITE_PACKAGE === "embedded") {
120+
// for the embedded package we always use the values from the URL as the widget host is responsible for analytics configuration
121+
apiKey = getUrlParams().posthogApiKey ?? undefined;
122+
apiHost = getUrlParams().posthogApiHost ?? undefined;
123+
} else if (import.meta.env.VITE_PACKAGE === "full") {
124+
// in full package it is the server responsible for the analytics
125+
apiKey = Config.get().posthog?.api_key;
126+
apiHost = Config.get().posthog?.api_host;
127+
}
131128

132-
this.posthog.init(posthogConfig.project_api_key, {
133-
api_host: posthogConfig.api_host,
129+
if (apiKey && apiHost) {
130+
this.posthog.init(apiKey, {
131+
api_host: apiHost,
134132
autocapture: false,
135133
mask_all_text: true,
136134
mask_all_element_attributes: true,
@@ -274,7 +272,7 @@ export class PosthogAnalytics {
274272
const client: MatrixClient = window.matrixclient;
275273
let accountAnalyticsId: string | null;
276274
if (widget) {
277-
accountAnalyticsId = getUrlParams().analyticsID;
275+
accountAnalyticsId = getUrlParams().posthogUserId;
278276
} else {
279277
const accountData = await client.getAccountDataFromServer(
280278
PosthogAnalytics.ANALYTICS_EVENT_TYPE,

src/config/ConfigOptions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,31 @@ Please see LICENSE in the repository root for full details.
88
export interface ConfigOptions {
99
/**
1010
* The Posthog endpoint to which analytics data will be sent.
11+
* This is only used in the full package of Element Call.
1112
*/
1213
posthog?: {
1314
api_key: string;
1415
api_host: string;
1516
};
1617
/**
1718
* The Sentry endpoint to which crash data will be sent.
19+
* This is only used in the full package of Element Call.
1820
*/
1921
sentry?: {
2022
DSN: string;
2123
environment: string;
2224
};
2325
/**
2426
* The rageshake server to which feedback and debug logs will be sent.
27+
* This is only used in the full package of Element Call.
2528
*/
2629
rageshake?: {
2730
submit_url: string;
2831
};
2932

3033
/**
3134
* Sets the URL to send opentelemetry data to. If unset, opentelemetry will
32-
* be disabled.
35+
* be disabled. This is only used in the full package of Element Call.
3336
*/
3437
opentelemetry?: {
3538
collector_url: string;

0 commit comments

Comments
 (0)