Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions __tests__/web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,50 @@ test("WTS web client omits $session_id when posthog-js has not loaded yet", asyn
expect(body.properties.custom).toBe("prop");
});

test("WTS web client warns and skips recording when sessionRecording lacks loadPostHog", async () => {
const spy = vi.spyOn(console, "warn").mockImplementation(() => {});

// No loadPostHog provided — must warn and skip without throwing. This path
// returns before the module-level "started" guard is set.
new WTS({
source: "site",
sessionRecording: { posthogKey: "phc_test", apiHost: "https://s.example.com" }
});

await new Promise(r => setTimeout(r, 10));
expect(spy).toHaveBeenCalledWith(expect.stringContaining("no loadPostHog"));
spy.mockRestore();
});

test("WTS web client loads posthog via loadPostHog and initializes recording", async () => {
const init = vi.fn();
const fakePosthog = { init, get_session_id: () => "sess-from-sdk" };
let loaderCalled = false;

new WTS({
source: "site",
sessionRecording: {
posthogKey: "phc_test",
apiHost: "https://s.example.com",
loadPostHog: async () => {
loaderCalled = true;
return { default: fakePosthog };
}
}
});

await new Promise(r => setTimeout(r, 10));
expect(loaderCalled).toBe(true);
expect(init).toHaveBeenCalledWith(
"phc_test",
expect.objectContaining({
api_host: "https://s.example.com",
capture_pageview: false,
disable_session_recording: false
})
);
});

test("WTS web client recovers id from localStorage when cookie is missing", () => {
storage.set("wts_did", "stored-id-123");

Expand Down
36 changes: 31 additions & 5 deletions src/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ interface PostHogLike {
get_session_id?: () => string | undefined;
}

/**
* What `loadPostHog` may resolve to: either the posthog-js module namespace
* (`{ default: posthog }`, as returned by `import("posthog-js")`) or the
* posthog instance directly.
*/
type PostHogModule = { default: PostHogLike } | PostHogLike;

const COOKIE_NAME = "wts_did";
const STORAGE_KEY = "wts_did";
const OPT_OUT_KEY = "WEBINY_TELEMETRY";
Expand All @@ -35,6 +42,15 @@ export interface SessionRecordingConfig {
apiHost: string;
/** CSS selector for text that should be masked in recordings. Defaults to `[data-private]`. */
maskTextSelector?: string;
/**
* Loads the posthog-js module. WTS never imports posthog-js itself — that way
* consumers which don't record (e.g. the Webiny admin app) don't trigger a
* build-time resolution of an uninstalled optional dependency. Recording
* consumers pass `() => import("posthog-js")` so the import resolves inside
* *their* bundle, where posthog-js is installed. Required to enable recording;
* if omitted, recording is skipped with a console warning.
*/
loadPostHog?: () => Promise<PostHogModule>;
}

export interface WebClientConfig extends ClientConfig {
Expand All @@ -48,7 +64,8 @@ export interface WebClientConfig extends ClientConfig {
/**
* Opt-in PostHog browser-side session recording. When omitted, posthog-js is not
* loaded — consumers that don't enable recording pay zero bundle cost. Consumers
* that do enable it must install `posthog-js` (declared as an optional peer dep).
* that do enable it must install `posthog-js` and pass `loadPostHog` (see
* {@link SessionRecordingConfig.loadPostHog}); WTS never imports it directly.
*/
sessionRecording?: SessionRecordingConfig;
/** Number of retry attempts for transient HTTP errors. Defaults to 3. */
Expand Down Expand Up @@ -239,17 +256,26 @@ export class WTS extends TelemetryClient {
);
return;
}
if (!cfg.loadPostHog) {
// eslint-disable-next-line no-console
console.warn(
"[wts] session recording is configured but no loadPostHog loader was provided — skipping. " +
"Provide loadPostHog (a loader that dynamically imports the posthog-js module) so the SDK resolves inside your bundle."
);
return;
}
const distinctId = this.identity.getDistinctId();
if (!distinctId) {
this.debug("no distinct_id available, skipping session recording");
return;
}
sessionRecordingStarted = true;

const mod = "posthog-js";
import(/* webpackIgnore: true */ /* @vite-ignore */ mod)
.then(({ default: posthog }) => {
this.posthog = posthog as PostHogLike;
cfg
.loadPostHog()
.then(mod => {
const posthog = ("default" in mod ? mod.default : mod) as PostHogLike;
this.posthog = posthog;
posthog.init(posthogKey, {
api_host: cfg.apiHost,
// WTS owns event capture via wts-server. The PostHog browser SDK is
Expand Down
Loading