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:
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user