Skip to content
Draft
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
7 changes: 6 additions & 1 deletion components/providers/posthog-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// app/providers.js
"use client";
import { disableAnalytics, initializeAnalytics } from "@/lib/analytics";
import {
disableAnalytics,
filterBrowserLoadFailureExceptions,
initializeAnalytics,
} from "@/lib/analytics";
import { env } from "@/lib/env";
import { useEffect } from "react";

Expand All @@ -21,6 +25,7 @@ export function CSPostHogProvider({

void initializeAnalytics(env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: posthogApiPath,
before_send: filterBrowserLoadFailureExceptions,
capture_dead_clicks: true,
capture_exceptions: {
capture_console_errors: true,
Expand Down
61 changes: 61 additions & 0 deletions lib/analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import { filterBrowserLoadFailureExceptions } from "./analytics";

describe("analytics event filtering", () => {
it("drops stackless Safari load failure exceptions", () => {
const event = {
event: "$exception",
properties: {
$browser: "Safari",
$exception_list: [
{
type: "TypeError",
value: "Load failed",
},
],
},
uuid: "stackless-safari-load-failure",
};

expect(filterBrowserLoadFailureExceptions(event)).toBeNull();
});

it("keeps Safari load failure exceptions with stack frames", () => {
const event = {
event: "$exception",
properties: {
$browser: "Safari",
$exception_list: [
{
stacktrace: {
frames: [{ filename: "app/page.tsx" }],
},
type: "TypeError",
value: "Load failed",
},
],
},
uuid: "safari-load-failure-with-stack",
};

expect(filterBrowserLoadFailureExceptions(event)).toBe(event);
});

it("keeps non-Safari load failure exceptions", () => {
const event = {
event: "$exception",
properties: {
$browser: "Chrome",
$exception_list: [
{
type: "TypeError",
value: "Load failed",
},
],
},
uuid: "chrome-load-failure",
};

expect(filterBrowserLoadFailureExceptions(event)).toBe(event);
});
});
53 changes: 52 additions & 1 deletion lib/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
"use client";

import { useSyncExternalStore } from "react";
import type { PostHog, PostHogConfig } from "posthog-js";
import type { BeforeSendFn, PostHog, PostHogConfig } from "posthog-js";

type CaptureProperties = Record<string, boolean | number | string>;
type GameResult = "lost" | "won";

interface ExceptionFrame {
filename?: unknown;
}

interface ExceptionEntry {
stacktrace?: {
frames?: ExceptionFrame[];
};
type?: unknown;
value?: unknown;
}

let isAnalyticsEnabled = false;
let posthogClientPromise: Promise<PostHog> | null = null;
const analyticsEnabledSubscribers = new Set<() => void>();
Expand Down Expand Up @@ -41,6 +53,45 @@ export function useAnalyticsEnabled() {
);
}

function isExceptionEntry(value: unknown): value is ExceptionEntry {
return typeof value === "object" && value !== null;
}

function hasStackFrames(exceptionList: ExceptionEntry[]) {
return exceptionList.some((exception) =>
exception.stacktrace?.frames?.some((frame) => frame.filename),
);
}

function getEventProperty(
properties: object | undefined,
key: string,
): unknown {
if (!properties) return undefined;

return (properties as Record<string, unknown>)[key];
}

export const filterBrowserLoadFailureExceptions: BeforeSendFn = (event) => {
if (event?.event !== "$exception") return event;

const properties = event.properties;
const exceptionList = getEventProperty(properties, "$exception_list");

if (!Array.isArray(exceptionList)) return event;

const normalizedExceptionList = exceptionList.filter(isExceptionEntry);
const isStacklessSafariLoadFailure =
getEventProperty(properties, "$browser") === "Safari" &&
normalizedExceptionList.some(
(exception) =>
exception.type === "TypeError" && exception.value === "Load failed",
) &&
!hasStackFrames(normalizedExceptionList);

return isStacklessSafariLoadFailure ? null : event;
};

export function initializeAnalytics(
apiKey: string,
options: Partial<PostHogConfig>,
Expand Down
Loading