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
13 changes: 13 additions & 0 deletions .github/workflows/dev-deploy-vercel-dashboard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ jobs:
run: vercel build --target=staging --token=${{ secrets.VERCEL_TOKEN }}
working-directory: echo/frontend

- name: Upload source maps to PostHog
continue-on-error: true
working-directory: echo/frontend
env:
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
POSTHOG_CLI_PROJECT_ID: "197841"
run: |
curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh
"$HOME/.posthog/posthog-cli" --host https://eu.posthog.com sourcemap process \
--directory .vercel/output/static \
--release-version "$GITHUB_SHA" \
--delete-after

- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --target=staging --token=${{ secrets.VERCEL_TOKEN }}
working-directory: echo/frontend
13 changes: 13 additions & 0 deletions .github/workflows/dev-deploy-vercel-portal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ jobs:
run: vercel build --target=staging --token=${{ secrets.VERCEL_TOKEN }}
working-directory: echo/frontend

- name: Upload source maps to PostHog
continue-on-error: true
working-directory: echo/frontend
env:
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
POSTHOG_CLI_PROJECT_ID: "197841"
run: |
curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh
"$HOME/.posthog/posthog-cli" --host https://eu.posthog.com sourcemap process \
--directory .vercel/output/static \
--release-version "$GITHUB_SHA" \
--delete-after

- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --target=staging --token=${{ secrets.VERCEL_TOKEN }}
working-directory: echo/frontend
13 changes: 13 additions & 0 deletions .github/workflows/prod-deploy-vercel-dashboard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ jobs:
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
working-directory: echo/frontend

- name: Upload source maps to PostHog
continue-on-error: true
working-directory: echo/frontend
env:
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
POSTHOG_CLI_PROJECT_ID: "160282"
run: |
curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh
"$HOME/.posthog/posthog-cli" --host https://eu.posthog.com sourcemap process \
--directory .vercel/output/static \
--release-version "$GITHUB_SHA" \
--delete-after

- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
working-directory: echo/frontend
13 changes: 13 additions & 0 deletions .github/workflows/prod-deploy-vercel-portal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ jobs:
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
working-directory: echo/frontend

- name: Upload source maps to PostHog
continue-on-error: true
working-directory: echo/frontend
env:
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
POSTHOG_CLI_PROJECT_ID: "160282"
run: |
curl --proto '=https' --tlsv1.2 -LsSf https://download.posthog.com/cli | sh
"$HOME/.posthog/posthog-cli" --host https://eu.posthog.com sourcemap process \
--directory .vercel/output/static \
--release-version "$GITHUB_SHA" \
--delete-after

- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
working-directory: echo/frontend
50 changes: 50 additions & 0 deletions echo/frontend/src/lib/errorTracking.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// @vitest-environment jsdom
// @vitest-environment-options { "url": "https://portal.dembrane.com/nl-NL/abc123/start" }

import type { CaptureResult } from "posthog-js";
import { expect, it } from "vitest";
import { dropInjectedScriptExceptions } from "./errorTracking";

const PAGE_URL = "https://portal.dembrane.com/nl-NL/abc123/start";

const exceptionEvent = (frames: Array<{ filename?: string }>): CaptureResult =>
({
event: "$exception",
properties: {
$exception_list: [{ stacktrace: { frames } }],
},
}) as unknown as CaptureResult;

it("drops an exception whose only frame is the document URL", () => {
const event = exceptionEvent([{ filename: PAGE_URL }]);
expect(dropInjectedScriptExceptions(event)).toBeNull();
});

it("keeps an exception with a frame that points at a script asset", () => {
const event = exceptionEvent([
{ filename: "https://portal.dembrane.com/assets/index-a1b2c3.js" },
]);
expect(dropInjectedScriptExceptions(event)).toBe(event);
});

it("keeps an exception that mixes a document frame with an asset frame", () => {
const event = exceptionEvent([
{ filename: PAGE_URL },
{ filename: "https://portal.dembrane.com/assets/index-a1b2c3.js" },
]);
expect(dropInjectedScriptExceptions(event)).toBe(event);
});

it("keeps an exception that has no stack frames", () => {
const event = exceptionEvent([]);
expect(dropInjectedScriptExceptions(event)).toBe(event);
});

it("ignores non-exception events", () => {
const event = { event: "$pageview", properties: {} } as CaptureResult;
expect(dropInjectedScriptExceptions(event)).toBe(event);
});

it("passes a null event through untouched", () => {
expect(dropInjectedScriptExceptions(null)).toBeNull();
});
56 changes: 56 additions & 0 deletions echo/frontend/src/lib/errorTracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// PostHog exception autocapture picks up errors thrown by scripts that in-app
// mobile browsers and extensions inject into the page (the "Script error." and
// window.__firefox__ family). Their only stack frame points at the HTML
// document itself, never at one of our bundled /assets/*.js chunks, so they are
// noise we can drop before it reaches error tracking. Real portal crashes keep
// frames that resolve to script assets and pass through untouched.

import type { CaptureResult } from "posthog-js";

interface StackFrame {
filename?: string;
}

interface CapturedException {
stacktrace?: {
frames?: StackFrame[];
};
}

// A frame belongs to the HTML document (not a script asset) when its filename
// resolves to the same origin and path as the current page.
const isDocumentFrame = (filename?: string): boolean => {
if (!filename) return false;
try {
const frameUrl = new URL(filename, window.location.href);
return (
frameUrl.origin === window.location.origin &&
frameUrl.pathname === window.location.pathname
);
} catch {
return false;
}
};

// Drops an $exception event whose every frame points at the document URL.
// Anything else, including events with no stack frames, passes through.
export const dropInjectedScriptExceptions = (
event: CaptureResult | null,
): CaptureResult | null => {
if (!event || event.event !== "$exception") return event;

const exceptions = event.properties?.$exception_list as
| CapturedException[]
| undefined;
if (!Array.isArray(exceptions)) return event;

const frames = exceptions.flatMap(
(exception) => exception?.stacktrace?.frames ?? [],
);
if (frames.length === 0) return event;

const everyFrameIsDocument = frames.every((frame) =>
isDocumentFrame(frame?.filename),
);
return everyFrameIsDocument ? null : event;
};
5 changes: 5 additions & 0 deletions echo/frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ import {
USE_PARTICIPANT_ROUTER,
} from "./config";
import { recoverFromChunkFailure } from "./lib/appVersion";
import { dropInjectedScriptExceptions } from "./lib/errorTracking";

posthog.init(POSTHOG_TOKEN, {
api_host: POSTHOG_HOST,
// Drop exceptions thrown by scripts that in-app mobile browsers and
// extensions inject into the page. Their only frame points at the document
// URL, so they arrive as unattributable noise next to real crashes.
before_send: dropInjectedScriptExceptions,
// Error tracking: autocapture unhandled errors and promise rejections.
// React render errors are reported separately via ErrorBoundary.
capture_exceptions: {
Expand Down
6 changes: 6 additions & 0 deletions echo/frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ export default defineConfig(({ mode }) => {
},
},
},
// "hidden" emits .map files without a sourceMappingURL comment, so the
// maps are never referenced from the served bundles. CI injects a chunk
// id and uploads them to PostHog error tracking, then deletes them (see
// the Vercel deploy workflows), so real portal crashes symbolicate
// instead of arriving minified.
sourcemap: "hidden",
},
define: {
__APP_BUILD_ID__: JSON.stringify(buildId),
Expand Down
Loading