mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-07 05:47:03 +00:00
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
This commit is contained in:
@@ -34,51 +34,64 @@ possible to support encryption.
|
||||
|
||||
## Widget within a messenger app
|
||||
|
||||
| Package | Deployment | URL |
|
||||
| -------- | ----------------------------- | ----------------------------------------------------------------------------- |
|
||||
| Full | All | `https://element_call.domain/room` |
|
||||
| Embedded | Remote URL | `https://element_call.domain/` n.b. no `/room` part |
|
||||
| Embedded | Embedded within messenger app | Platform dependent, but you load the `index.html` file without a `/room` part |
|
||||
| Package | Deployment | URL |
|
||||
| ------------------------------------ | ----------------------------- | ----------------------------------------------------------------------------- |
|
||||
| [Full](./embedded-standalone.md) | All | `https://element_call.domain/room` |
|
||||
| [Embedded](./embedded-standalone.md) | Remote URL | `https://element_call.domain/` n.b. no `/room` part |
|
||||
| [Embedded](./embedded-standalone.md) | Embedded within messenger app | Platform dependent, but you load the `index.html` file without a `/room` part |
|
||||
|
||||
## Parameters
|
||||
|
||||
### Common Parameters
|
||||
|
||||
These parameters are relevant to both widget and standalone modes:
|
||||
These parameters are relevant to both [widget](./embedded-standalone.md) and [standalone](./embedded-standalone.md) modes:
|
||||
|
||||
| Name | Values | Required for widget | Required for SPA | Description |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesn’t provide any. |
|
||||
| `analyticsID` | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. |
|
||||
| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. |
|
||||
| `confineToRoom` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Keeps the user confined to the current call/room. |
|
||||
| `displayName` | | No | No | Display name used for auto-registration. |
|
||||
| `enableE2EE` (deprecated) | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Legacy flag to enable end-to-end encryption, not used in the `livekit` branch. |
|
||||
| `fontScale` | A decimal number such as `0.9` | No, defaults to `1.0` | No, defaults to `1.0` | Factor by which to scale the interface's font size. |
|
||||
| `fonts` | | No | No | Defines the font(s) used by the interface. Multiple font parameters can be specified: `?font=font-one&font=font-two...`. |
|
||||
| `hideHeader` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the room header when in a call. |
|
||||
| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. |
|
||||
| `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. |
|
||||
| `intent` | `start_call` or `join_existing` | No, defaults to `start_call` | No, defaults to `start_call` | The intent of the user with respect to the call. e.g. if they clicked a Start Call button, this would be `start_call`. If it was a Join Call button, it would be `join_existing`. |
|
||||
| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. |
|
||||
| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) |
|
||||
| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. |
|
||||
| `roomId` | [Matrix Room ID](https://spec.matrix.org/v1.12/appendices/#room-ids) | Yes | No | Anything about what room we're pointed to should be from useRoomIdentifier which parses the path and resolves alias with respect to the default server name, however roomId is an exception as we need the room ID in embedded widget mode, and not the room alias (or even the via params because we are not trying to join it). This is also not validated, where it is in `useRoomIdentifier()`. |
|
||||
| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. |
|
||||
| `skipLobby` (deprecated: use `intent` instead) | `true` or `false` | No. If `intent` is explicitly `start_call` then defaults to `true`. Otherwise defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) |
|
||||
| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. |
|
||||
| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. |
|
||||
| Name | Values | Required for widget | Required for SPA | Description |
|
||||
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `allowIceFallback` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Allows use of fallback STUN servers for ICE if the user's homeserver doesn’t provide any. |
|
||||
| `analyticsID` (deprecated: use `posthogUserId` instead) | Posthog analytics ID | No | No | Available only with user's consent for sharing telemetry in Element Web. |
|
||||
| `appPrompt` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Prompts the user to launch the native mobile app upon entering a room, applicable only on Android and iOS, and must be enabled in config. |
|
||||
| `confineToRoom` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Keeps the user confined to the current call/room. |
|
||||
| `displayName` | | No | No | Display name used for auto-registration. |
|
||||
| `enableE2EE` (deprecated) | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Legacy flag to enable end-to-end encryption, not used in the `livekit` branch. |
|
||||
| `fontScale` | A decimal number such as `0.9` | No, defaults to `1.0` | No, defaults to `1.0` | Factor by which to scale the interface's font size. |
|
||||
| `fonts` | | No | No | Defines the font(s) used by the interface. Multiple font parameters can be specified: `?font=font-one&font=font-two...`. |
|
||||
| `hideHeader` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the room header when in a call. |
|
||||
| `hideScreensharing` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Hides the screen-sharing button. |
|
||||
| `homeserver` | | Not applicable | No | Homeserver for registering a new (guest) user, configures non-default guest user server when creating a spa link. |
|
||||
| `intent` | `start_call` or `join_existing` | No, defaults to `start_call` | No, defaults to `start_call` | The intent of the user with respect to the call. e.g. if they clicked a Start Call button, this would be `start_call`. If it was a Join Call button, it would be `join_existing`. |
|
||||
| `lang` | [BCP 47](https://www.rfc-editor.org/info/bcp47) code | No | No | The language the app should use. |
|
||||
| `password` | | No | No | E2EE password when using a shared secret. (For individual sender keys in embedded mode this is not required.) |
|
||||
| `perParticipantE2EE` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables per participant encryption with Keys exchanged over encrypted matrix room messages. |
|
||||
| `roomId` | [Matrix Room ID](https://spec.matrix.org/v1.12/appendices/#room-ids) | Yes | No | Anything about what room we're pointed to should be from useRoomIdentifier which parses the path and resolves alias with respect to the default server name, however roomId is an exception as we need the room ID in embedded widget mode, and not the room alias (or even the via params because we are not trying to join it). This is also not validated, where it is in `useRoomIdentifier()`. |
|
||||
| `showControls` | `true` or `false` | No, defaults to `true` | No, defaults to `true` | Displays controls like mute, screen-share, invite, and hangup buttons during a call. |
|
||||
| `skipLobby` (deprecated: use `intent` instead) | `true` or `false` | No. If `intent` is explicitly `start_call` then defaults to `true`. Otherwise defaults to `false` | No, defaults to `false` | Skips the lobby to join a call directly, can be combined with preload in widget. When `true` the audio and video inputs will be muted by default. (This means there currently is no way to start without muted video if one wants to skip the lobby. Also not in widget mode.) |
|
||||
| `theme` | One of: `light`, `dark`, `light-high-contrast`, `dark-high-contrast` | No, defaults to `dark` | No, defaults to `dark` | UI theme to use. |
|
||||
| `viaServers` | Comma separated list of [Matrix Server Names](https://spec.matrix.org/v1.12/appendices/#server-name) | Not applicable | No | Homeserver for joining a room, non-empty value required for rooms not on the user’s default homeserver. |
|
||||
|
||||
### Widget-only parameters
|
||||
|
||||
These parameters are only available in widget mode.
|
||||
These parameters are only supported in [widget](./embedded-standalone.md) mode.
|
||||
|
||||
| Name | Values | Required | Description |
|
||||
| --------------- | ----------------------------------------------------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `baseUrl` | | Yes | The base URL of the homeserver to use for media lookups. |
|
||||
| `deviceId` | Matrix device ID | Yes | The Matrix device ID for the widget host. |
|
||||
| `parentUrl` | | Yes | The url used to send widget action postMessages. This should be the domain of the client or the webview the widget is hosted in. (in case the widget is not in an Iframe but in a dedicated webview we send the postMessages same WebView the widget lives in. Filtering is done in the widget so it ignores the messages it receives from itself) |
|
||||
| `posthogUserId` | Posthog user identifier | No | This replaces the `analyticsID` parameter |
|
||||
| `preload` | `true` or `false` | No, defaults to `false` | Pauses app before joining a call until an `io.element.join` widget action is seen, allowing preloading. |
|
||||
| `returnToLobby` | `true` or `false` | No, defaults to `false` | Displays the lobby in widget mode after leaving a call; shows a blank page if set to `false`. Useful for video rooms. |
|
||||
| `userId` | [Matrix User Identifier](https://spec.matrix.org/v1.12/appendices/#user-identifiers) | Yes | The Matrix user ID. |
|
||||
| `widgetId` | [MSC2774](https://github.com/matrix-org/matrix-spec-proposals/pull/2774) format widget ID | Yes | The id used by the widget. The presence of this parameter implies that element call will not connect to a homeserver directly and instead tries to establish postMessage communication via the `parentUrl`. |
|
||||
|
||||
### Embedded-only parameters
|
||||
|
||||
These parameters are only supported in the [embedded](./embedded-standalone.md) package of Element Call and will be ignored in the [full](./embedded-standalone.md) package.
|
||||
|
||||
| Name | Values | Required | Description |
|
||||
| -------------------- | -------------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `posthogApiHost` | Posthog server URL | No | e.g. `https://posthog-element-call.element.io`. Only supported in embedded package. In full package the value from config is used. |
|
||||
| `posthogApiKey` | Posthog project API key | No | Only supported in embedded package. In full package the value from config is used. |
|
||||
| `rageshakeSubmitUrl` | Rageshake server URL endpoint | No | e.g. `https://element.io/bugreports/submit`. In full package the value from config is used. |
|
||||
| `sentryDsn` | Sentry [DSN](https://docs.sentry.io/concepts/key-terms/dsn-explainer/) | No | In full package the value from config is used. |
|
||||
| `sentryEnvironment` | Sentry [environment](https://docs.sentry.io/concepts/key-terms/key-terms/) | No | In full package the value from config is used. |
|
||||
|
||||
@@ -105,7 +105,15 @@ export interface UrlParams {
|
||||
/**
|
||||
* The Posthog analytics ID. It is only available if the user has given consent for sharing telemetry in element web.
|
||||
*/
|
||||
analyticsID: string | null;
|
||||
posthogUserId: string | null;
|
||||
/**
|
||||
* The Posthog API host. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
posthogApiHost: string | null;
|
||||
/**
|
||||
* The Posthog API key. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
posthogApiKey: string | null;
|
||||
/**
|
||||
* Whether the app is allowed to use fallback STUN servers for ICE in case the
|
||||
* user's homeserver doesn't provide any.
|
||||
@@ -155,6 +163,20 @@ export interface UrlParams {
|
||||
* If it was a Join Call button, it would be `join_existing`.
|
||||
*/
|
||||
intent: string | null;
|
||||
|
||||
/**
|
||||
* The rageshake submit URL. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
rageshakeSubmitUrl: string | null;
|
||||
|
||||
/**
|
||||
* The Sentry DSN. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
sentryDsn: string | null;
|
||||
/**
|
||||
* The Sentry environment. This is only used in the embedded package of Element Call.
|
||||
*/
|
||||
sentryEnvironment: string | null;
|
||||
}
|
||||
|
||||
// This is here as a stopgap, but what would be far nicer is a function that
|
||||
@@ -257,7 +279,6 @@ export const getUrlParams = (
|
||||
lang: parser.getParam("lang"),
|
||||
fonts: parser.getAllParams("font"),
|
||||
fontScale: Number.isNaN(fontScale) ? null : fontScale,
|
||||
analyticsID: parser.getParam("analyticsID"),
|
||||
allowIceFallback: parser.getFlagParam("allowIceFallback"),
|
||||
perParticipantE2EE: parser.getFlagParam("perParticipantE2EE"),
|
||||
skipLobby: parser.getFlagParam(
|
||||
@@ -271,6 +292,13 @@ export const getUrlParams = (
|
||||
viaServers: !isWidget ? parser.getParam("viaServers") : null,
|
||||
homeserver: !isWidget ? parser.getParam("homeserver") : null,
|
||||
intent,
|
||||
posthogApiHost: parser.getParam("posthogApiHost"),
|
||||
posthogApiKey: parser.getParam("posthogApiKey"),
|
||||
posthogUserId:
|
||||
parser.getParam("posthogUserId") ?? parser.getParam("analyticsID"),
|
||||
rageshakeSubmitUrl: parser.getParam("rageshakeSubmitUrl"),
|
||||
sentryDsn: parser.getParam("sentryDsn"),
|
||||
sentryEnvironment: parser.getParam("sentryEnvironment"),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
91
src/analytics/PosthogAnalytics.test.ts
Normal file
91
src/analytics/PosthogAnalytics.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
expect,
|
||||
describe,
|
||||
it,
|
||||
vi,
|
||||
beforeEach,
|
||||
beforeAll,
|
||||
afterAll,
|
||||
} from "vitest";
|
||||
|
||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||
import { mockConfig } from "../utils/test";
|
||||
|
||||
describe("PosthogAnalytics", () => {
|
||||
describe("embedded package", () => {
|
||||
beforeAll(() => {
|
||||
vi.stubEnv("VITE_PACKAGE", "embedded");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig({});
|
||||
window.location.hash = "#";
|
||||
PosthogAnalytics.resetInstance();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("does not create instance without config value or URL params", () => {
|
||||
expect(PosthogAnalytics.instance.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores config value and does not create instance", () => {
|
||||
mockConfig({
|
||||
posthog: {
|
||||
api_host: "https://api.example.com.localhost",
|
||||
api_key: "api_key",
|
||||
},
|
||||
});
|
||||
expect(PosthogAnalytics.instance.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it("uses URL params if both set", () => {
|
||||
window.location.hash = `#?posthogApiHost=${encodeURIComponent("https://url.example.com.localhost")}&posthogApiKey=api_key`;
|
||||
expect(PosthogAnalytics.instance.isEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("full package", () => {
|
||||
beforeAll(() => {
|
||||
vi.stubEnv("VITE_PACKAGE", "full");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig({});
|
||||
window.location.hash = "#";
|
||||
PosthogAnalytics.resetInstance();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("does not create instance without config value", () => {
|
||||
expect(PosthogAnalytics.instance.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores URL params and does not create instance", () => {
|
||||
window.location.hash = `#?posthogApiHost=${encodeURIComponent("https://url.example.com.localhost")}&posthogApiKey=api_key`;
|
||||
expect(PosthogAnalytics.instance.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it("creates instance with config value", () => {
|
||||
mockConfig({
|
||||
posthog: {
|
||||
api_host: "https://api.example.com.localhost",
|
||||
api_key: "api_key",
|
||||
},
|
||||
});
|
||||
expect(PosthogAnalytics.instance.isEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -71,11 +71,6 @@ interface PlatformProperties {
|
||||
cryptoVersion?: string;
|
||||
}
|
||||
|
||||
interface PosthogSettings {
|
||||
project_api_key?: string;
|
||||
api_host?: string;
|
||||
}
|
||||
|
||||
export class PosthogAnalytics {
|
||||
/* Wrapper for Posthog analytics.
|
||||
* 3 modes of anonymity are supported, governed by this.anonymity
|
||||
@@ -113,24 +108,27 @@ export class PosthogAnalytics {
|
||||
return this.internalInstance;
|
||||
}
|
||||
|
||||
public static resetInstance(): void {
|
||||
// Reset the singleton instance
|
||||
this.internalInstance = null;
|
||||
}
|
||||
|
||||
private constructor(private readonly posthog: PostHog) {
|
||||
const posthogConfig: PosthogSettings = {
|
||||
project_api_key: Config.get().posthog?.api_key,
|
||||
api_host: Config.get().posthog?.api_host,
|
||||
};
|
||||
let apiKey: string | undefined;
|
||||
let apiHost: string | undefined;
|
||||
if (import.meta.env.VITE_PACKAGE === "embedded") {
|
||||
// for the embedded package we always use the values from the URL as the widget host is responsible for analytics configuration
|
||||
apiKey = getUrlParams().posthogApiKey ?? undefined;
|
||||
apiHost = getUrlParams().posthogApiHost ?? undefined;
|
||||
} else if (import.meta.env.VITE_PACKAGE === "full") {
|
||||
// in full package it is the server responsible for the analytics
|
||||
apiKey = Config.get().posthog?.api_key;
|
||||
apiHost = Config.get().posthog?.api_host;
|
||||
}
|
||||
|
||||
if (posthogConfig.project_api_key && posthogConfig.api_host) {
|
||||
if (
|
||||
PosthogAnalytics.getPlatformProperties().matrixBackend === "embedded"
|
||||
) {
|
||||
const { analyticsID } = getUrlParams();
|
||||
// if the embedding platform (element web) already got approval to communicating with posthog
|
||||
// element call can also send events to posthog
|
||||
optInAnalytics.setValue(Boolean(analyticsID));
|
||||
}
|
||||
|
||||
this.posthog.init(posthogConfig.project_api_key, {
|
||||
api_host: posthogConfig.api_host,
|
||||
if (apiKey && apiHost) {
|
||||
this.posthog.init(apiKey, {
|
||||
api_host: apiHost,
|
||||
autocapture: false,
|
||||
mask_all_text: true,
|
||||
mask_all_element_attributes: true,
|
||||
@@ -274,7 +272,7 @@ export class PosthogAnalytics {
|
||||
const client: MatrixClient = window.matrixclient;
|
||||
let accountAnalyticsId: string | null;
|
||||
if (widget) {
|
||||
accountAnalyticsId = getUrlParams().analyticsID;
|
||||
accountAnalyticsId = getUrlParams().posthogUserId;
|
||||
} else {
|
||||
const accountData = await client.getAccountDataFromServer(
|
||||
PosthogAnalytics.ANALYTICS_EVENT_TYPE,
|
||||
|
||||
@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
|
||||
export interface ConfigOptions {
|
||||
/**
|
||||
* The Posthog endpoint to which analytics data will be sent.
|
||||
* This is only used in the full package of Element Call.
|
||||
*/
|
||||
posthog?: {
|
||||
api_key: string;
|
||||
@@ -15,6 +16,7 @@ export interface ConfigOptions {
|
||||
};
|
||||
/**
|
||||
* The Sentry endpoint to which crash data will be sent.
|
||||
* This is only used in the full package of Element Call.
|
||||
*/
|
||||
sentry?: {
|
||||
DSN: string;
|
||||
@@ -22,6 +24,7 @@ export interface ConfigOptions {
|
||||
};
|
||||
/**
|
||||
* The rageshake server to which feedback and debug logs will be sent.
|
||||
* This is only used in the full package of Element Call.
|
||||
*/
|
||||
rageshake?: {
|
||||
submit_url: string;
|
||||
@@ -29,7 +32,7 @@ export interface ConfigOptions {
|
||||
|
||||
/**
|
||||
* Sets the URL to send opentelemetry data to. If unset, opentelemetry will
|
||||
* be disabled.
|
||||
* be disabled. This is only used in the full package of Element Call.
|
||||
*/
|
||||
opentelemetry?: {
|
||||
collector_url: string;
|
||||
|
||||
@@ -5,24 +5,158 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from "vitest";
|
||||
|
||||
import { Initializer } from "../src/initializer";
|
||||
import { mockConfig } from "./utils/test";
|
||||
|
||||
test("initBeforeReact sets font family from URL param", async () => {
|
||||
window.location.hash = "#?font=DejaVu Sans";
|
||||
await Initializer.initBeforeReact();
|
||||
expect(
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
"--font-family",
|
||||
),
|
||||
).toBe('"DejaVu Sans"');
|
||||
});
|
||||
|
||||
test("initBeforeReact sets font scale from URL param", async () => {
|
||||
window.location.hash = "#?fontScale=1.2";
|
||||
await Initializer.initBeforeReact();
|
||||
expect(
|
||||
getComputedStyle(document.documentElement).getPropertyValue("--font-scale"),
|
||||
).toBe("1.2");
|
||||
const sentryInitSpy = vi.fn();
|
||||
|
||||
// Place the mock after the spy is defined
|
||||
vi.mock("@sentry/react", () => ({
|
||||
init: sentryInitSpy,
|
||||
reactRouterV7BrowserTracingIntegration: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("Initializer", async () => {
|
||||
// we import here to make sure that Sentry is mocked first
|
||||
const { Initializer } = await import("./initializer.tsx");
|
||||
describe("initBeforeReact()", () => {
|
||||
it("sets font family from URL param", async () => {
|
||||
window.location.hash = "#?font=DejaVu Sans";
|
||||
await Initializer.initBeforeReact();
|
||||
expect(
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
"--font-family",
|
||||
),
|
||||
).toBe('"DejaVu Sans"');
|
||||
});
|
||||
|
||||
it("sets font scale from URL param", async () => {
|
||||
window.location.hash = "#?fontScale=1.2";
|
||||
await Initializer.initBeforeReact();
|
||||
expect(
|
||||
getComputedStyle(document.documentElement).getPropertyValue(
|
||||
"--font-scale",
|
||||
),
|
||||
).toBe("1.2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("init()", () => {
|
||||
describe("sentry setup", () => {
|
||||
describe("embedded package", () => {
|
||||
beforeAll(() => {
|
||||
vi.stubEnv("VITE_PACKAGE", "embedded");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig({});
|
||||
window.location.hash = "#";
|
||||
Initializer.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sentryInitSpy.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("does not call Sentry.init() without config value", async () => {
|
||||
await Initializer.init();
|
||||
expect(sentryInitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores config value and does not create instance", async () => {
|
||||
mockConfig({
|
||||
sentry: {
|
||||
DSN: "https://config.example.com.localhost",
|
||||
environment: "config",
|
||||
},
|
||||
});
|
||||
await Initializer.init();
|
||||
expect(sentryInitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses sentryDsn param if set", async () => {
|
||||
window.location.hash = `#?sentryDsn=${encodeURIComponent("https://dsn.example.com.localhost")}`;
|
||||
await Initializer.init();
|
||||
expect(sentryInitSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dsn: "https://dsn.example.com.localhost",
|
||||
environment: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses sentryDsn and sentryEnvironment params if set", async () => {
|
||||
window.location.hash = `#?sentryDsn=${encodeURIComponent("https://dsn.example.com.localhost")}&sentryEnvironment=fooEnvironment`;
|
||||
await Initializer.init();
|
||||
expect(sentryInitSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dsn: "https://dsn.example.com.localhost",
|
||||
environment: "fooEnvironment",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("full package", () => {
|
||||
beforeAll(() => {
|
||||
vi.stubEnv("VITE_PACKAGE", "full");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig({});
|
||||
window.location.hash = "#";
|
||||
Initializer.reset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sentryInitSpy.mockClear();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("does not create instance without config value or URL param", async () => {
|
||||
await Initializer.init();
|
||||
expect(sentryInitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores URL params and does not create instance", async () => {
|
||||
window.location.hash = `#?sentryDsn=${encodeURIComponent("https://dsn.example.com.localhost")}&sentryEnvironment=fooEnvironment`;
|
||||
await Initializer.init();
|
||||
expect(sentryInitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates instance with config value", async () => {
|
||||
mockConfig({
|
||||
sentry: {
|
||||
DSN: "https://dsn.example.com.localhost",
|
||||
environment: "fooEnvironment",
|
||||
},
|
||||
});
|
||||
await Initializer.init();
|
||||
expect(sentryInitSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dsn: "https://dsn.example.com.localhost",
|
||||
environment: "fooEnvironment",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,11 +109,11 @@ class DependencyLoadStates {
|
||||
}
|
||||
|
||||
export class Initializer {
|
||||
private static internalInstance: Initializer;
|
||||
private static internalInstance: Initializer | undefined;
|
||||
private isInitialized = false;
|
||||
|
||||
public static isInitialized(): boolean {
|
||||
return Initializer.internalInstance?.isInitialized;
|
||||
return !!Initializer.internalInstance?.isInitialized;
|
||||
}
|
||||
|
||||
public static async initBeforeReact(): Promise<void> {
|
||||
@@ -193,11 +193,19 @@ export class Initializer {
|
||||
Initializer.internalInstance.initPromise = new Promise<void>((resolve) => {
|
||||
// initStep calls itself recursively until everything is initialized in the correct order.
|
||||
// Then the promise gets resolved.
|
||||
Initializer.internalInstance.initStep(resolve);
|
||||
Initializer.internalInstance?.initStep(resolve);
|
||||
});
|
||||
return Initializer.internalInstance.initPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the initializer. This is used in tests to ensure that the initializer
|
||||
* is re-initialized for each test.
|
||||
*/
|
||||
public static reset(): void {
|
||||
Initializer.internalInstance = undefined;
|
||||
}
|
||||
|
||||
private loadStates = new DependencyLoadStates();
|
||||
|
||||
private initStep(resolve: (value: void | PromiseLike<void>) => void): void {
|
||||
@@ -220,10 +228,22 @@ export class Initializer {
|
||||
this.loadStates.sentry === LoadState.None &&
|
||||
this.loadStates.config === LoadState.Loaded
|
||||
) {
|
||||
if (Config.get().sentry?.DSN && Config.get().sentry?.environment) {
|
||||
let dsn: string | undefined;
|
||||
let environment: string | undefined;
|
||||
if (import.meta.env.VITE_PACKAGE === "embedded") {
|
||||
// for the embedded package we always use the values from the URL as the widget host is responsible for analytics configuration
|
||||
dsn = getUrlParams().sentryDsn ?? undefined;
|
||||
environment = getUrlParams().sentryEnvironment ?? undefined;
|
||||
}
|
||||
if (import.meta.env.VITE_PACKAGE === "full") {
|
||||
// in full package it is the server responsible for the analytics
|
||||
dsn = Config.get().sentry?.DSN;
|
||||
environment = Config.get().sentry?.environment;
|
||||
}
|
||||
if (dsn) {
|
||||
Sentry.init({
|
||||
dsn: Config.get().sentry?.DSN,
|
||||
environment: Config.get().sentry?.environment,
|
||||
dsn,
|
||||
environment,
|
||||
integrations: [
|
||||
Sentry.reactRouterV7BrowserTracingIntegration({
|
||||
useEffect: React.useEffect,
|
||||
|
||||
79
src/otel/otel.test.ts
Normal file
79
src/otel/otel.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
expect,
|
||||
describe,
|
||||
it,
|
||||
vi,
|
||||
beforeEach,
|
||||
beforeAll,
|
||||
afterAll,
|
||||
} from "vitest";
|
||||
|
||||
import { ElementCallOpenTelemetry } from "./otel";
|
||||
import { mockConfig } from "../utils/test";
|
||||
|
||||
describe("ElementCallOpenTelemetry", () => {
|
||||
describe("embedded package", () => {
|
||||
beforeAll(() => {
|
||||
vi.stubEnv("VITE_PACKAGE", "embedded");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig({});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("does not create instance without config value", () => {
|
||||
ElementCallOpenTelemetry.globalInit();
|
||||
expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores config value and does not create instance", () => {
|
||||
mockConfig({
|
||||
opentelemetry: {
|
||||
collector_url: "https://collector.example.com.localhost",
|
||||
},
|
||||
});
|
||||
ElementCallOpenTelemetry.globalInit();
|
||||
expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("full package", () => {
|
||||
beforeAll(() => {
|
||||
vi.stubEnv("VITE_PACKAGE", "full");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig({});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("does not create instance without config value", () => {
|
||||
ElementCallOpenTelemetry.globalInit();
|
||||
expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("creates instance with config value", () => {
|
||||
mockConfig({
|
||||
opentelemetry: {
|
||||
collector_url: "https://collector.example.com.localhost",
|
||||
},
|
||||
});
|
||||
ElementCallOpenTelemetry.globalInit();
|
||||
expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { PosthogSpanProcessor } from "../analytics/PosthogSpanProcessor";
|
||||
import { Config } from "../config/Config";
|
||||
import { RageshakeSpanProcessor } from "../analytics/RageshakeSpanProcessor";
|
||||
import { getRageshakeSubmitUrl } from "../settings/submit-rageshake";
|
||||
|
||||
const SERVICE_NAME = "element-call";
|
||||
|
||||
@@ -28,20 +29,24 @@ export class ElementCallOpenTelemetry {
|
||||
public readonly rageshakeProcessor?: RageshakeSpanProcessor;
|
||||
|
||||
public static globalInit(): void {
|
||||
const config = Config.get();
|
||||
// this is only supported in the full package as the is currently no support for passing in the collector URL from the widget host
|
||||
const collectorUrl =
|
||||
import.meta.env.VITE_PACKAGE === "full"
|
||||
? Config.get().opentelemetry?.collector_url
|
||||
: undefined;
|
||||
// we always enable opentelemetry in general. We only enable the OTLP
|
||||
// collector if a URL is defined (and in future if another setting is defined)
|
||||
// Posthog reporting is enabled or disabled
|
||||
// within the posthog code.
|
||||
const shouldEnableOtlp = Boolean(config.opentelemetry?.collector_url);
|
||||
const shouldEnableOtlp = Boolean(collectorUrl);
|
||||
|
||||
if (!sharedInstance || sharedInstance.isOtlpEnabled !== shouldEnableOtlp) {
|
||||
logger.info("(Re)starting OpenTelemetry debug reporting");
|
||||
sharedInstance?.dispose();
|
||||
|
||||
sharedInstance = new ElementCallOpenTelemetry(
|
||||
config.opentelemetry?.collector_url,
|
||||
config.rageshake?.submit_url,
|
||||
collectorUrl,
|
||||
getRageshakeSubmitUrl(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@ interface Props {
|
||||
|
||||
export const FeedbackSettingsTab: FC<Props> = ({ roomId }) => {
|
||||
const { t } = useTranslation();
|
||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||
const { submitRageshake, sending, sent, error, available } =
|
||||
useSubmitRageshake();
|
||||
const sendRageshakeRequest = useRageshakeRequest();
|
||||
|
||||
const onSubmitFeedback = useCallback(
|
||||
@@ -66,20 +67,27 @@ export const FeedbackSettingsTab: FC<Props> = ({ roomId }) => {
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>{t("common.analytics")}</h4>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="optInAnalytics"
|
||||
type="checkbox"
|
||||
checked={optInAnalytics ?? undefined}
|
||||
description={optInDescription}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>): void => {
|
||||
setOptInAnalytics?.(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
</FieldRow>
|
||||
// in the embedded package the widget host is responsible for analytics consent
|
||||
const analyticsConsentBlock =
|
||||
import.meta.env.VITE_PACKAGE === "embedded" ? null : (
|
||||
<>
|
||||
<h4>{t("common.analytics")}</h4>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="optInAnalytics"
|
||||
type="checkbox"
|
||||
checked={optInAnalytics ?? undefined}
|
||||
description={optInDescription}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>): void => {
|
||||
setOptInAnalytics?.(event.target.checked);
|
||||
}}
|
||||
/>
|
||||
</FieldRow>
|
||||
</>
|
||||
);
|
||||
|
||||
const feedbackBlock = available ? (
|
||||
<>
|
||||
<h4>{t("settings.feedback_tab_h4")}</h4>
|
||||
<Text>{t("settings.feedback_tab_body")}</Text>
|
||||
<form onSubmit={onSubmitFeedback}>
|
||||
@@ -113,6 +121,13 @@ export const FeedbackSettingsTab: FC<Props> = ({ roomId }) => {
|
||||
{sent && <Text>{t("settings.feedback_tab_thank_you")}</Text>}
|
||||
</FieldRow>
|
||||
</form>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{analyticsConsentBlock}
|
||||
{feedbackBlock}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
||||
import { Slider } from "../Slider";
|
||||
import { DeviceSelection } from "./DeviceSelection";
|
||||
import { DeveloperSettingsTab } from "./DeveloperSettingsTab";
|
||||
import { isRageshakeAvailable } from "./submit-rageshake";
|
||||
|
||||
type SettingsTab =
|
||||
| "audio"
|
||||
@@ -146,7 +147,12 @@ export const SettingsModal: FC<Props> = ({
|
||||
|
||||
const tabs = [audioTab, videoTab];
|
||||
if (widget === null) tabs.push(profileTab);
|
||||
tabs.push(preferencesTab, feedbackTab);
|
||||
tabs.push(preferencesTab);
|
||||
if (isRageshakeAvailable() || import.meta.env.VITE_PACKAGE === "full") {
|
||||
// for full package we want to show the analytics consent checkbox
|
||||
// even if rageshake is not available
|
||||
tabs.push(feedbackTab);
|
||||
}
|
||||
if (showDeveloperSettingsTab) tabs.push(developerTab);
|
||||
|
||||
return (
|
||||
|
||||
153
src/settings/submit-rageshake.test.ts
Normal file
153
src/settings/submit-rageshake.test.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
expect,
|
||||
describe,
|
||||
it,
|
||||
afterEach,
|
||||
vi,
|
||||
type Mock,
|
||||
beforeEach,
|
||||
} from "vitest";
|
||||
|
||||
import {
|
||||
getRageshakeSubmitUrl,
|
||||
isRageshakeAvailable,
|
||||
} from "./submit-rageshake";
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
import { mockConfig } from "../utils/test";
|
||||
|
||||
vi.mock("../UrlParams", () => ({ getUrlParams: vi.fn() }));
|
||||
|
||||
describe("isRageshakeAvailable", () => {
|
||||
beforeEach(() => {
|
||||
(getUrlParams as Mock).mockReturnValue({});
|
||||
mockConfig({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("embedded package", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("VITE_PACKAGE", "embedded");
|
||||
});
|
||||
|
||||
it("returns false with no rageshakeSubmitUrl URL param", () => {
|
||||
expect(isRageshakeAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores config value and returns false with no rageshakeSubmitUrl URL param", () => {
|
||||
mockConfig({
|
||||
rageshake: {
|
||||
submit_url: "https://config.example.com.localhost",
|
||||
},
|
||||
});
|
||||
expect(isRageshakeAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true with rageshakeSubmitUrl URL param", () => {
|
||||
(getUrlParams as Mock).mockReturnValue({
|
||||
rageshakeSubmitUrl: "https://url.example.com.localhost",
|
||||
});
|
||||
expect(isRageshakeAvailable()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("full package", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("VITE_PACKAGE", "full");
|
||||
});
|
||||
it("returns false with no config value", () => {
|
||||
expect(isRageshakeAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores rageshakeSubmitUrl URL param and returns false with no config value", () => {
|
||||
(getUrlParams as Mock).mockReturnValue({
|
||||
rageshakeSubmitUrl: "https://url.example.com.localhost",
|
||||
});
|
||||
expect(isRageshakeAvailable()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true with config value", () => {
|
||||
mockConfig({
|
||||
rageshake: {
|
||||
submit_url: "https://config.example.com.localhost",
|
||||
},
|
||||
});
|
||||
expect(isRageshakeAvailable()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRageshakeSubmitUrl", () => {
|
||||
beforeEach(() => {
|
||||
(getUrlParams as Mock).mockReturnValue({});
|
||||
mockConfig({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("embedded package", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("VITE_PACKAGE", "embedded");
|
||||
});
|
||||
|
||||
it("returns undefined no rageshakeSubmitUrl URL param", () => {
|
||||
expect(getRageshakeSubmitUrl()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns rageshakeSubmitUrl URL param when set", () => {
|
||||
(getUrlParams as Mock).mockReturnValue({
|
||||
rageshakeSubmitUrl: "https://url.example.com.localhost",
|
||||
});
|
||||
expect(getRageshakeSubmitUrl()).toBe("https://url.example.com.localhost");
|
||||
});
|
||||
|
||||
it("ignores config param and returns undefined", () => {
|
||||
mockConfig({
|
||||
rageshake: {
|
||||
submit_url: "https://config.example.com.localhost",
|
||||
},
|
||||
});
|
||||
expect(getRageshakeSubmitUrl()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("full package", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("VITE_PACKAGE", "full");
|
||||
});
|
||||
it("returns undefined with no config value", () => {
|
||||
expect(getRageshakeSubmitUrl()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("ignores rageshakeSubmitUrl URL param and returns undefined", () => {
|
||||
(getUrlParams as Mock).mockReturnValue({
|
||||
rageshakeSubmitUrl: "https://url.example.com.localhost",
|
||||
});
|
||||
expect(getRageshakeSubmitUrl()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns config value when set", () => {
|
||||
mockConfig({
|
||||
rageshake: {
|
||||
submit_url: "https://config.example.com.localhost",
|
||||
},
|
||||
});
|
||||
expect(getRageshakeSubmitUrl()).toBe(
|
||||
"https://config.example.com.localhost",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import { useClient } from "../ClientContext";
|
||||
import { Config } from "../config/Config";
|
||||
import { ElementCallOpenTelemetry } from "../otel/otel";
|
||||
import { type RageshakeRequestModal } from "../room/RageshakeRequestModal";
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
|
||||
const gzip = async (text: string): Promise<Blob> => {
|
||||
// pako is relatively large (200KB), so we only import it when needed
|
||||
@@ -116,11 +117,30 @@ interface RageShakeSubmitOptions {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function getRageshakeSubmitUrl(): string | undefined {
|
||||
if (import.meta.env.VITE_PACKAGE === "full") {
|
||||
// in full package we always use the one configured on the server
|
||||
return Config.get().rageshake?.submit_url;
|
||||
}
|
||||
|
||||
if (import.meta.env.VITE_PACKAGE === "embedded") {
|
||||
// in embedded package we always use the one provided by the widget host
|
||||
return getUrlParams().rageshakeSubmitUrl ?? undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isRageshakeAvailable(): boolean {
|
||||
return !!getRageshakeSubmitUrl();
|
||||
}
|
||||
|
||||
export function useSubmitRageshake(): {
|
||||
submitRageshake: (opts: RageShakeSubmitOptions) => Promise<void>;
|
||||
sending: boolean;
|
||||
sent: boolean;
|
||||
error?: Error;
|
||||
available: boolean;
|
||||
} {
|
||||
const { client } = useClient();
|
||||
|
||||
@@ -138,7 +158,7 @@ export function useSubmitRageshake(): {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
async (opts) => {
|
||||
if (!Config.get().rageshake?.submit_url) {
|
||||
if (!getRageshakeSubmitUrl()) {
|
||||
throw new Error("No rageshake URL is configured");
|
||||
}
|
||||
|
||||
@@ -297,6 +317,7 @@ export function useSubmitRageshake(): {
|
||||
sending,
|
||||
sent,
|
||||
error,
|
||||
available: isRageshakeAvailable(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -264,6 +264,8 @@ export function mockConfig(config: Partial<ResolvedConfigOptions> = {}): void {
|
||||
...DEFAULT_CONFIG,
|
||||
...config,
|
||||
});
|
||||
// simulate loading the config
|
||||
vi.spyOn(Config, "init").mockResolvedValue(void 0);
|
||||
}
|
||||
|
||||
export class MockRTCSession extends TypedEventEmitter<
|
||||
|
||||
@@ -16,6 +16,11 @@ import basicSsl from "@vitejs/plugin-basic-ssl";
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode, packageType }) => {
|
||||
const env = loadEnv(mode, process.cwd());
|
||||
// Environment variables with the VITE_ prefix are accessible at runtime.
|
||||
// So, we set this to allow for build/package specific behaviour.
|
||||
// In future we might be able to do what is needed via code splitting at
|
||||
// build time.
|
||||
process.env.VITE_PACKAGE = packageType ?? "full";
|
||||
const plugins = [
|
||||
react(),
|
||||
basicSsl(),
|
||||
@@ -32,7 +37,7 @@ export default defineConfig(({ mode, packageType }) => {
|
||||
inject: {
|
||||
data: {
|
||||
brand: env.VITE_PRODUCT_NAME || "Element Call",
|
||||
packageType: packageType ?? "full",
|
||||
packageType: process.env.VITE_PACKAGE,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user