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
4 changes: 4 additions & 0 deletions docs/analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ exception carries `route_name` too.
`:code` in every property of every event, nested ones included
(`$current_url`, `$pathname`, `$referrer`, `$initial_*`, exceptions,
session replay snapshots).
- Chunk-load errors are not sent: `before_send`
(`dropChunkLoadErrors`) drops an exception whose entries are all
chunk-load errors. `chunkReload` handles them, but a programmatic
`router.push()` still rejects and posthog-js would report it as unhandled.

## Person properties

Expand Down
16 changes: 15 additions & 1 deletion src/lib/analytics/usePostHog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useAnalyticsConsent } from "@/lib/analytics/useAnalyticsConsent";

// Util
import { redact } from "@/util/data";
import { isChunkLoadError } from "@/lib/chunkReload";

// Types & Interfaces
import type { BeforeSendFn, PostHog, Properties } from "posthog-js";
Expand Down Expand Up @@ -50,6 +51,19 @@ export const maskUrlCodes: BeforeSendFn = (event) => {
};
};

/**
* before_send: drops exceptions that are only chunk-load errors. chunkReload
* already handles those (reload or message), but vue-router still rejects
* the router.push() that hit them, and posthog-js reports that rejection as
* unhandled after every deploy.
*/
export const dropChunkLoadErrors: BeforeSendFn = (event) => {
if (event?.event !== "$exception") return event;
const list = event.properties.$exception_list;
if (!Array.isArray(list) || !list.length) return event;
return list.every((entry) => isChunkLoadError(entry?.value)) ? null : event;
};

// storage keys PostHog writes: its own, the opt-out flag and survey state
const POSTHOG_STORAGE_KEY =
/^((__)?ph_|seenSurvey_|inProgressSurvey_|lastSeenSurveyDate$)/;
Expand Down Expand Up @@ -169,7 +183,7 @@ async function startAnalytics(): Promise<void> {
// no cookie
persistence: "localStorage",
respect_dnt: true,
before_send: maskUrlCodes,
before_send: [maskUrlCodes, dropChunkLoadErrors],
});

// register global versions
Expand Down
53 changes: 51 additions & 2 deletions src/tests/lib/analytics/usePostHog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,12 +502,16 @@ describe("usePostHog", () => {
) => ({ uuid: "u", event: name, properties, ...extra });

it("is passed to posthog.init as before_send", async () => {
const { maskUrlCodes } = await load({ consent: "granted" });
const { maskUrlCodes, dropChunkLoadErrors } = await load({
consent: "granted",
});
await settle();

expect(posthog.init).toHaveBeenCalledWith(
"phc_test",
expect.objectContaining({ before_send: maskUrlCodes })
expect.objectContaining({
before_send: [maskUrlCodes, dropChunkLoadErrors],
})
);
});

Expand Down Expand Up @@ -609,6 +613,51 @@ describe("usePostHog", () => {
});
});

describe("dropChunkLoadErrors", () => {
const CHUNK =
"Failed to fetch dynamically imported module: https://prunplanner.org/assets/chunks/HomepageView.abc123.js";
const exception = (...values: string[]) => ({
uuid: "u",
event: "$exception",
properties: {
$exception_list: values.map((value) => ({
type: "TypeError",
value,
mechanism: { handled: false },
})),
},
});

it("drops an exception that is only a chunk-load error", async () => {
const { dropChunkLoadErrors } = await load();

expect(dropChunkLoadErrors(exception(CHUNK))).toBeNull();
});

it("keeps other exceptions, mixed ones included", async () => {
const { dropChunkLoadErrors } = await load();
const other = exception("Cannot read properties of undefined");
const mixed = exception(CHUNK, "Cannot read properties of undefined");

expect(dropChunkLoadErrors(other)).toBe(other);
expect(dropChunkLoadErrors(mixed)).toBe(mixed);
});

it("keeps other events and exceptions without a list", async () => {
const { dropChunkLoadErrors } = await load();
const pageview = {
uuid: "u",
event: "$pageview",
properties: { message: CHUNK },
};
const empty = { uuid: "u", event: "$exception", properties: {} };

expect(dropChunkLoadErrors(null)).toBeNull();
expect(dropChunkLoadErrors(pageview)).toBe(pageview);
expect(dropChunkLoadErrors(empty)).toBe(empty);
});
});

// deny relies on posthog-js dropping requests while `__loaded` is false
describe("real posthog-js", () => {
async function captureWithRealPostHog() {
Expand Down
Loading