@weatherboard/errors 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Weatherboard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # @weatherboard/errors
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Products never import `@sentry/*` directly.
4
+
5
+ Entry points:
6
+ - `./next`: `withErrors(nextConfig, opts)` wraps Next config, exposes `$SENTRY_BUILD_ENVIRONMENT` and `$SENTRY_BUILD_RELEASE`, and uploads source maps only with all three upload variables. Maps are deleted after upload; budget builds can set `PERF_BUDGET_SOURCE_MAPS=keep`.
7
+ - `./next/server`: `register` and `onRequestError` for server instrumentation.
8
+ - `./next/client`: deferred browser initialization and `onRouterTransitionStart`. Buffers early errors and rejections, loading immediately on the first error; otherwise waits for load and idle. Breadcrumbs before SDK initialization are unavailable.
9
+ - `./next/global-error`: `GlobalError` reports render failures and renders its required `children`; the product supplies the fallback document and design.
10
+ - `./client`: `reportHandled(error, { where, extra })` for catches that show a banner or toast. Fingerprints use `where` and message. Never throws; before the deferred browser SDK loads, queues the error and its context and requests the SDK immediately. With no configured loader or a disabled SDK, it is a no-op. The `where` tag identifies the catch site.
11
+ - `./noise`: shared patterns with their justifying issue ids.
12
+ - `./verify`: `verifyErrors(appDirectory)` registers the product wiring test.
13
+
14
+ The five configuration variables are `SENTRY_DSN` (server), `PUBLIC_SENTRY_DSN` (browser), `SENTRY_ORG`, `SENTRY_PROJECT`, and `SENTRY_AUTH_TOKEN` (secret, build only). The wrapper inlines only the public DSN. Reporting requires a DSN and `VERCEL_ENV=production` or `preview`; development and unset environments do not initialize. Release comes from `VERCEL_GIT_COMMIT_SHA`, never `NODE_ENV`. Tracing is off and default PII is disabled.
15
+
16
+ Wire a Next app in four files:
17
+
18
+ 1. In `next.config.mjs`: import `{ withErrors }` from `@weatherboard/errors/next` and export `withErrors(nextConfig)`.
19
+ 2. In `instrumentation.ts`: export `{ register, onRequestError }` from `@weatherboard/errors/next/server`.
20
+ 3. In `instrumentation-client.ts`: export `{ onRouterTransitionStart }` from `@weatherboard/errors/next/client`.
21
+ 4. In the app's global error boundary, mark it `"use client"`, import `{ GlobalError }` from `@weatherboard/errors/next/global-error`, and render `<GlobalError error={error}><html><body>your fallback</body></html></GlobalError>`.
22
+
23
+ Add a node:test file calling `verifyErrors` with the app directory. HTTP helpers are reserved for a future `./http` entry; Workers and React Native are outside this version.
@@ -0,0 +1,15 @@
1
+ export type LoadWindow = {
2
+ readonly document: {
3
+ readonly readyState: string;
4
+ };
5
+ addEventListener(type: "load", listener: () => void, options: {
6
+ once: true;
7
+ }): void;
8
+ setTimeout(handler: () => void, timeout: number): unknown;
9
+ readonly requestIdleCallback?: (callback: () => void, options: {
10
+ timeout: number;
11
+ }) => unknown;
12
+ };
13
+ export declare const IDLE_TIMEOUT_MS = 2000;
14
+ export declare const IDLE_FALLBACK_MS = 200;
15
+ export declare function afterLoadAndIdle(win: LoadWindow, run: () => void): void;
package/after-load.js ADDED
@@ -0,0 +1,18 @@
1
+ const IDLE_TIMEOUT_MS = 2e3;
2
+ const IDLE_FALLBACK_MS = 200;
3
+ function afterLoadAndIdle(win, run) {
4
+ const whenIdle = () => {
5
+ if (typeof win.requestIdleCallback === "function") {
6
+ win.requestIdleCallback(run, { timeout: IDLE_TIMEOUT_MS });
7
+ } else {
8
+ win.setTimeout(run, IDLE_FALLBACK_MS);
9
+ }
10
+ };
11
+ if (win.document.readyState === "complete") whenIdle();
12
+ else win.addEventListener("load", whenIdle, { once: true });
13
+ }
14
+ export {
15
+ IDLE_FALLBACK_MS,
16
+ IDLE_TIMEOUT_MS,
17
+ afterLoadAndIdle
18
+ };
@@ -0,0 +1,3 @@
1
+ import * as Sentry from "@sentry/nextjs";
2
+ export declare const captureException: typeof Sentry.captureException;
3
+ export declare const captureRouterTransitionStart: typeof Sentry.captureRouterTransitionStart;
package/browser-sdk.js ADDED
@@ -0,0 +1,27 @@
1
+ import * as Sentry from "@sentry/nextjs";
2
+ import { attachReporter } from "./client.js";
3
+ import { SENTRY_IGNORE_ERRORS } from "./noise.js";
4
+ Sentry.init({
5
+ dsn: process.env.PUBLIC_SENTRY_DSN,
6
+ environment: $SENTRY_BUILD_ENVIRONMENT,
7
+ release: $SENTRY_BUILD_RELEASE || void 0,
8
+ tracesSampleRate: 0,
9
+ sendDefaultPii: false,
10
+ defaultIntegrations: false,
11
+ integrations: [
12
+ Sentry.globalHandlersIntegration(),
13
+ Sentry.dedupeIntegration(),
14
+ Sentry.breadcrumbsIntegration(),
15
+ Sentry.linkedErrorsIntegration(),
16
+ Sentry.functionToStringIntegration(),
17
+ Sentry.browserSessionIntegration()
18
+ ],
19
+ ignoreErrors: SENTRY_IGNORE_ERRORS
20
+ });
21
+ attachReporter(Sentry);
22
+ const captureException = Sentry.captureException;
23
+ const captureRouterTransitionStart = Sentry.captureRouterTransitionStart;
24
+ export {
25
+ captureException,
26
+ captureRouterTransitionStart
27
+ };
package/client.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { type CaptureContext } from "./sentry-deferred.js";
2
+ export type HandledContext = {
3
+ where: string;
4
+ extra?: Record<string, unknown>;
5
+ };
6
+ type Reporter = {
7
+ isEnabled(): boolean;
8
+ captureException(error: unknown, context: CaptureContext): unknown;
9
+ };
10
+ /** Internal SDK registration shared by server and browser entry points. */
11
+ export declare function attachReporter(reporter: Reporter): void;
12
+ /** UI catch sites must never fail while reporting the original error. No SDK import on this path. */
13
+ export declare function reportHandled(error: unknown, context: HandledContext): void;
14
+ export {};
package/client.js ADDED
@@ -0,0 +1,21 @@
1
+ import { captureException } from "./sentry-deferred.js";
2
+ const KEY = /* @__PURE__ */ Symbol.for("weatherboard.errors.reporter");
3
+ const holder = globalThis;
4
+ function attachReporter(reporter) {
5
+ holder[KEY] = reporter;
6
+ }
7
+ function reportHandled(error, context) {
8
+ try {
9
+ const sdk = holder[KEY];
10
+ if (sdk && !sdk.isEnabled()) return;
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ const captureContext = { fingerprint: [context.where, message], tags: { where: context.where }, extra: context.extra };
13
+ if (sdk) sdk.captureException(error, captureContext);
14
+ else captureException(error, captureContext);
15
+ } catch {
16
+ }
17
+ }
18
+ export {
19
+ attachReporter,
20
+ reportHandled
21
+ };
@@ -0,0 +1,2 @@
1
+ /** Only deployed Vercel environments report errors; NODE_ENV is deliberately irrelevant. */
2
+ export declare function deployedEnvironment(value: string | undefined): "production" | "preview" | null;
package/environment.js ADDED
@@ -0,0 +1,6 @@
1
+ function deployedEnvironment(value) {
2
+ return value === "production" || value === "preview" ? value : null;
3
+ }
4
+ export {
5
+ deployedEnvironment
6
+ };
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from "react";
2
+ /** Reporting only. The product owns its document and fallback design. */
3
+ export declare function GlobalError({ error, children }: {
4
+ error: Error & {
5
+ digest?: string;
6
+ };
7
+ children: ReactNode;
8
+ }): ReactNode;
9
+ export default GlobalError;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { useEffect } from "react";
3
+ import { captureException } from "./sentry-deferred.js";
4
+ function GlobalError({ error, children }) {
5
+ useEffect(() => {
6
+ captureException(error);
7
+ }, [error]);
8
+ return children;
9
+ }
10
+ var global_error_default = GlobalError;
11
+ export {
12
+ GlobalError,
13
+ global_error_default as default
14
+ };
@@ -0,0 +1 @@
1
+ export { onRouterTransitionStart } from "./sentry-deferred.js";
@@ -0,0 +1,13 @@
1
+ import { afterLoadAndIdle } from "./after-load.js";
2
+ import { bufferEarlyErrors, registerSentryLoader, loadSentryNow } from "./sentry-deferred.js";
3
+ import { onRouterTransitionStart } from "./sentry-deferred.js";
4
+ if (typeof window !== "undefined" && process.env.PUBLIC_SENTRY_DSN && typeof $SENTRY_BUILD_ENVIRONMENT !== "undefined" && ($SENTRY_BUILD_ENVIRONMENT === "production" || $SENTRY_BUILD_ENVIRONMENT === "preview")) {
5
+ registerSentryLoader(() => import("./browser-sdk.js"));
6
+ bufferEarlyErrors(window);
7
+ afterLoadAndIdle(window, () => {
8
+ void loadSentryNow();
9
+ });
10
+ }
11
+ export {
12
+ onRouterTransitionStart
13
+ };
package/next.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** @param {import('next').NextConfig} nextConfig
2
+ * @param {{ silent?: boolean }} [opts]
3
+ * @returns {import('next').NextConfig} */
4
+ export function withErrors(nextConfig: import("next").NextConfig, opts?: {
5
+ silent?: boolean;
6
+ }): import("next").NextConfig;
package/next.js ADDED
@@ -0,0 +1,47 @@
1
+ import { withSentryConfig } from "@sentry/nextjs/config";
2
+ function withErrors(nextConfig, opts = {}) {
3
+ const required = ["SENTRY_ORG", "SENTRY_PROJECT", "SENTRY_AUTH_TOKEN"];
4
+ const missing = required.filter((name) => !process.env[name]);
5
+ const upload = missing.length === 0;
6
+ if (!upload) console.info(`[errors] source maps will NOT be uploaded: ${missing.join(", ")} not set.`);
7
+ const constants = {
8
+ $SENTRY_BUILD_ENVIRONMENT: process.env.VERCEL_ENV ?? "development",
9
+ $SENTRY_BUILD_RELEASE: process.env.VERCEL_GIT_COMMIT_SHA ?? "",
10
+ __SENTRY_DEBUG__: false,
11
+ __SENTRY_TRACING__: false,
12
+ __RRWEB_EXCLUDE_SHADOW_DOM__: true,
13
+ __RRWEB_EXCLUDE_IFRAME__: true,
14
+ __SENTRY_EXCLUDE_REPLAY_WORKER__: true
15
+ };
16
+ return withSentryConfig({
17
+ ...nextConfig,
18
+ env: { ...nextConfig.env, PUBLIC_SENTRY_DSN: process.env.PUBLIC_SENTRY_DSN ?? "" },
19
+ compiler: { ...nextConfig.compiler, define: { ...nextConfig.compiler?.define, ...constants } },
20
+ productionBrowserSourceMaps: true,
21
+ webpack(config, context) {
22
+ const configured = nextConfig.webpack ? nextConfig.webpack(config, context) : config;
23
+ (configured.plugins ??= []).push(new context.webpack.DefinePlugin(Object.fromEntries(
24
+ Object.entries(constants).map(([key, value]) => [key, JSON.stringify(value)])
25
+ )));
26
+ return configured;
27
+ }
28
+ }, {
29
+ org: upload ? process.env.SENTRY_ORG : void 0,
30
+ project: upload ? process.env.SENTRY_PROJECT : void 0,
31
+ authToken: upload ? process.env.SENTRY_AUTH_TOKEN : void 0,
32
+ sourcemaps: { disable: !upload, deleteSourcemapsAfterUpload: process.env.PERF_BUDGET_SOURCE_MAPS !== "keep" },
33
+ telemetry: false,
34
+ silent: opts.silent ?? false,
35
+ bundleSizeOptimizations: {
36
+ excludeDebugStatements: true,
37
+ excludeTracing: true,
38
+ excludeReplayShadowDom: true,
39
+ excludeReplayIframe: true,
40
+ excludeReplayWorker: true
41
+ },
42
+ webpack: { treeshake: { removeDebugLogging: true }, automaticVercelMonitors: false }
43
+ });
44
+ }
45
+ export {
46
+ withErrors
47
+ };
package/noise.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ export declare const NOISE_ENTRIES: readonly [{
2
+ readonly issue: "A-806";
3
+ readonly pattern: RegExp;
4
+ }, {
5
+ readonly issue: "A-806";
6
+ readonly pattern: RegExp;
7
+ }, {
8
+ readonly issue: "A-806";
9
+ readonly pattern: RegExp;
10
+ }, {
11
+ readonly issue: "A-806";
12
+ readonly pattern: RegExp;
13
+ }, {
14
+ readonly issue: "A-806";
15
+ readonly pattern: RegExp;
16
+ }, {
17
+ readonly issue: "A-806";
18
+ readonly pattern: RegExp;
19
+ }, {
20
+ readonly issue: "A-806";
21
+ readonly pattern: RegExp;
22
+ }, {
23
+ readonly issue: "A-806";
24
+ readonly pattern: "AbortError: The operation was aborted.";
25
+ }, {
26
+ readonly issue: "A-806";
27
+ readonly pattern: "AbortError: Fetch is aborted";
28
+ }, {
29
+ readonly issue: "A-806";
30
+ readonly pattern: "AbortError: signal is aborted without reason";
31
+ }, {
32
+ readonly issue: "A-806";
33
+ readonly pattern: "TypeError: Load failed";
34
+ }, {
35
+ readonly issue: "A-806";
36
+ readonly pattern: "TypeError: Failed to fetch";
37
+ }, {
38
+ readonly issue: "A-806";
39
+ readonly pattern: "TypeError: NetworkError when attempting to fetch resource.";
40
+ }, {
41
+ readonly issue: "A-806";
42
+ readonly pattern: "TypeError: cancelled";
43
+ }, {
44
+ readonly issue: "A-806";
45
+ readonly pattern: "ResizeObserver loop limit exceeded";
46
+ }, {
47
+ readonly issue: "A-806";
48
+ readonly pattern: "ResizeObserver loop completed with undelivered notifications";
49
+ }, {
50
+ readonly issue: "A-806";
51
+ readonly pattern: RegExp;
52
+ }, {
53
+ readonly issue: "A-806";
54
+ readonly pattern: RegExp;
55
+ }];
56
+ export declare const SENTRY_IGNORE_ERRORS: (string | RegExp)[];
57
+ export declare function isIgnoredMessage(message: string, patterns?: (string | RegExp)[]): boolean;
package/noise.js ADDED
@@ -0,0 +1,29 @@
1
+ const NOISE_ENTRIES = [
2
+ { issue: "A-806", pattern: /window\.__firefox__/ },
3
+ { issue: "A-806", pattern: /Can't find variable: __firefox__/ },
4
+ { issue: "A-806", pattern: /Invalid call to runtime\.sendMessage\(\)\. Tab not found/i },
5
+ { issue: "A-806", pattern: /Extension context invalidated/i },
6
+ { issue: "A-806", pattern: /chrome-extension:\/\// },
7
+ { issue: "A-806", pattern: /moz-extension:\/\// },
8
+ { issue: "A-806", pattern: /window\.ethereum/i },
9
+ { issue: "A-806", pattern: "AbortError: The operation was aborted." },
10
+ { issue: "A-806", pattern: "AbortError: Fetch is aborted" },
11
+ { issue: "A-806", pattern: "AbortError: signal is aborted without reason" },
12
+ { issue: "A-806", pattern: "TypeError: Load failed" },
13
+ { issue: "A-806", pattern: "TypeError: Failed to fetch" },
14
+ { issue: "A-806", pattern: "TypeError: NetworkError when attempting to fetch resource." },
15
+ { issue: "A-806", pattern: "TypeError: cancelled" },
16
+ { issue: "A-806", pattern: "ResizeObserver loop limit exceeded" },
17
+ { issue: "A-806", pattern: "ResizeObserver loop completed with undelivered notifications" },
18
+ { issue: "A-806", pattern: /Object Not Found Matching Id:\d+, MethodName:update/ },
19
+ { issue: "A-806", pattern: /Non-Error promise rejection captured/ }
20
+ ];
21
+ const SENTRY_IGNORE_ERRORS = NOISE_ENTRIES.map((entry) => entry.pattern);
22
+ function isIgnoredMessage(message, patterns = SENTRY_IGNORE_ERRORS) {
23
+ return patterns.some((pattern) => typeof pattern === "string" ? message.includes(pattern) : pattern.test(message));
24
+ }
25
+ export {
26
+ NOISE_ENTRIES,
27
+ SENTRY_IGNORE_ERRORS,
28
+ isIgnoredMessage
29
+ };
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@weatherboard/errors",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Shared Next.js and browser error reporting for Weatherboard products.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Weatherboard-Studio/weatherboard-studio.git",
9
+ "directory": "packages/errors"
10
+ },
11
+ "type": "module",
12
+ "dependencies": {
13
+ "@sentry/nextjs": "10.75.0"
14
+ },
15
+ "peerDependencies": {
16
+ "next": "^16.0.0",
17
+ "react": "^19.0.0"
18
+ },
19
+ "sideEffects": [
20
+ "./instrumentation-client.js",
21
+ "./browser-sdk.js"
22
+ ],
23
+ "exports": {
24
+ "./next": {
25
+ "types": "./next.d.ts",
26
+ "import": "./next.js"
27
+ },
28
+ "./next/server": {
29
+ "types": "./server.d.ts",
30
+ "import": "./server.js"
31
+ },
32
+ "./next/client": {
33
+ "types": "./instrumentation-client.d.ts",
34
+ "import": "./instrumentation-client.js"
35
+ },
36
+ "./next/global-error": {
37
+ "types": "./global-error.d.ts",
38
+ "import": "./global-error.js"
39
+ },
40
+ "./client": {
41
+ "types": "./client.d.ts",
42
+ "import": "./client.js"
43
+ },
44
+ "./noise": {
45
+ "types": "./noise.d.ts",
46
+ "import": "./noise.js"
47
+ },
48
+ "./verify": {
49
+ "types": "./verify.d.ts",
50
+ "import": "./verify.js"
51
+ }
52
+ },
53
+ "files": [
54
+ "*.js",
55
+ "*.d.ts",
56
+ "README.md",
57
+ "LICENSE"
58
+ ]
6
59
  }
@@ -0,0 +1,32 @@
1
+ export type CaptureContext = {
2
+ mechanism: {
3
+ type: string;
4
+ handled: boolean;
5
+ };
6
+ } | {
7
+ fingerprint: string[];
8
+ tags: Record<string, string>;
9
+ extra?: Record<string, unknown>;
10
+ };
11
+ export type DeferredSentrySdk = {
12
+ captureException(error: unknown, hint?: CaptureContext): unknown;
13
+ captureRouterTransitionStart(href: string, navigationType: string): void;
14
+ };
15
+ export type ErrorTarget = {
16
+ addEventListener(type: "error" | "unhandledrejection", listener: (event: unknown) => void): void;
17
+ removeEventListener(type: "error" | "unhandledrejection", listener: (event: unknown) => void): void;
18
+ };
19
+ export declare const ONERROR_MECHANISM: {
20
+ readonly type: "auto.browser.global_handlers.onerror";
21
+ readonly handled: false;
22
+ };
23
+ export declare const ONREJECTION_MECHANISM: {
24
+ readonly type: "auto.browser.global_handlers.onunhandledrejection";
25
+ readonly handled: false;
26
+ };
27
+ export declare function bufferEarlyErrors(target: ErrorTarget): void;
28
+ export declare function registerSentryLoader(load: () => Promise<DeferredSentrySdk>): void;
29
+ export declare function loadSentryNow(): Promise<DeferredSentrySdk | null>;
30
+ export declare function captureException(error: unknown, context?: CaptureContext): void;
31
+ export declare function onRouterTransitionStart(href: string, navigationType: string): void;
32
+ export declare function resetSentryDeferredForTests(): void;
@@ -0,0 +1,101 @@
1
+ const ONERROR_MECHANISM = { type: "auto.browser.global_handlers.onerror", handled: false };
2
+ const ONREJECTION_MECHANISM = { type: "auto.browser.global_handlers.onunhandledrejection", handled: false };
3
+ const KEY = /* @__PURE__ */ Symbol.for("weatherboard.sentry-deferred");
4
+ function state() {
5
+ const holder = globalThis;
6
+ let current = holder[KEY];
7
+ if (current === void 0) {
8
+ current = { sdk: null, queue: [], load: null, loading: null, detach: null };
9
+ holder[KEY] = current;
10
+ }
11
+ return current;
12
+ }
13
+ function errorOf(event) {
14
+ const value = event;
15
+ if (value !== null && typeof value === "object") {
16
+ if (value.error !== void 0 && value.error !== null) return value.error;
17
+ if (typeof value.message === "string") return value.message;
18
+ }
19
+ return event;
20
+ }
21
+ function reasonOf(event) {
22
+ const value = event;
23
+ return value !== null && typeof value === "object" && "reason" in value ? value.reason : event;
24
+ }
25
+ function bufferEarlyErrors(target) {
26
+ const current = state();
27
+ if (current.detach !== null || current.sdk !== null) return;
28
+ const onError = (event) => record({ error: errorOf(event), context: { mechanism: ONERROR_MECHANISM } });
29
+ const onRejection = (event) => record({ error: reasonOf(event), context: { mechanism: ONREJECTION_MECHANISM } });
30
+ target.addEventListener("error", onError);
31
+ target.addEventListener("unhandledrejection", onRejection);
32
+ current.detach = () => {
33
+ target.removeEventListener("error", onError);
34
+ target.removeEventListener("unhandledrejection", onRejection);
35
+ };
36
+ }
37
+ function record(pending) {
38
+ const current = state();
39
+ if (current.sdk !== null) {
40
+ current.sdk.captureException(pending.error, pending.context);
41
+ return;
42
+ }
43
+ current.queue.push(pending);
44
+ void loadSentryNow();
45
+ }
46
+ function registerSentryLoader(load) {
47
+ state().load = load;
48
+ }
49
+ function loadSentryNow() {
50
+ const current = state();
51
+ if (current.loading !== null) return current.loading;
52
+ if (current.load === null) return Promise.resolve(null);
53
+ current.loading = current.load().then(
54
+ (sdk) => {
55
+ current.sdk = sdk;
56
+ current.detach?.();
57
+ current.detach = null;
58
+ const queued = current.queue.splice(0);
59
+ for (const pending of queued) {
60
+ sdk.captureException(pending.error, pending.context);
61
+ }
62
+ return sdk;
63
+ },
64
+ (error) => {
65
+ console.info("[sentry] the error-reporting SDK failed to load; errors on this page are not being reported.", error);
66
+ return null;
67
+ }
68
+ );
69
+ return current.loading;
70
+ }
71
+ function captureException(error, context) {
72
+ const current = state();
73
+ if (current.load === null && current.sdk === null) return;
74
+ if (current.sdk !== null) {
75
+ current.sdk.captureException(error, context);
76
+ return;
77
+ }
78
+ record({ error, context });
79
+ }
80
+ function onRouterTransitionStart(href, navigationType) {
81
+ state().sdk?.captureRouterTransitionStart(href, navigationType);
82
+ }
83
+ function resetSentryDeferredForTests() {
84
+ const current = state();
85
+ current.detach?.();
86
+ current.sdk = null;
87
+ current.queue = [];
88
+ current.load = null;
89
+ current.loading = null;
90
+ current.detach = null;
91
+ }
92
+ export {
93
+ ONERROR_MECHANISM,
94
+ ONREJECTION_MECHANISM,
95
+ bufferEarlyErrors,
96
+ captureException,
97
+ loadSentryNow,
98
+ onRouterTransitionStart,
99
+ registerSentryLoader,
100
+ resetSentryDeferredForTests
101
+ };
@@ -0,0 +1,4 @@
1
+ import * as SentryModule from "@sentry/nextjs";
2
+ declare const Sentry: typeof SentryModule;
3
+ export declare function initialiseServer(overrides?: Pick<Parameters<typeof Sentry.init>[0] & {}, "transport">): void;
4
+ export {};
package/server-sdk.js ADDED
@@ -0,0 +1,23 @@
1
+ import * as SentryModule from "@sentry/nextjs";
2
+ const Sentry = SentryModule.default ?? SentryModule;
3
+ import { attachReporter } from "./client.js";
4
+ import { deployedEnvironment } from "./environment.js";
5
+ import { SENTRY_IGNORE_ERRORS } from "./noise.js";
6
+ function initialiseServer(overrides = {}) {
7
+ const environment = deployedEnvironment(process.env.VERCEL_ENV);
8
+ const dsn = process.env.SENTRY_DSN;
9
+ if (!dsn || !environment) return;
10
+ Sentry.init({
11
+ dsn,
12
+ environment,
13
+ release: process.env.VERCEL_GIT_COMMIT_SHA || void 0,
14
+ tracesSampleRate: 0,
15
+ sendDefaultPii: false,
16
+ ignoreErrors: SENTRY_IGNORE_ERRORS,
17
+ ...overrides
18
+ });
19
+ attachReporter(Sentry);
20
+ }
21
+ export {
22
+ initialiseServer
23
+ };
package/server.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { captureRequestError as onRequestError } from "@sentry/nextjs";
2
+ export declare function register(options?: Pick<import("@sentry/nextjs").NodeOptions, "transport">): Promise<void>;
package/server.js ADDED
@@ -0,0 +1,12 @@
1
+ import { deployedEnvironment } from "./environment.js";
2
+ import { captureRequestError } from "@sentry/nextjs";
3
+ async function register(options = {}) {
4
+ if (process.env.NEXT_RUNTIME !== "nodejs") return;
5
+ if (!process.env.SENTRY_DSN || !deployedEnvironment(process.env.VERCEL_ENV)) return;
6
+ const { initialiseServer } = await import("./server-sdk.js");
7
+ initialiseServer(options);
8
+ }
9
+ export {
10
+ captureRequestError as onRequestError,
11
+ register
12
+ };
package/verify.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Assert wiring directly; exported separately so a fixture can prove failure. */
2
+ export declare function assertErrorWiring(appDirectory: string): void;
3
+ /** Call from a product's node:test file with the absolute app directory. */
4
+ export declare function verifyErrors(appDirectory: string): void;
package/verify.js ADDED
@@ -0,0 +1,33 @@
1
+ import assert from "node:assert/strict";
2
+ import { readdirSync, readFileSync, existsSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { test } from "node:test";
5
+ function sources(dir) {
6
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
7
+ if (["node_modules", ".next", ".git", "dist"].includes(entry.name)) return [];
8
+ const path = join(dir, entry.name);
9
+ if (entry.isDirectory()) return sources(path);
10
+ return /\.[cm]?[jt]sx?$/.test(entry.name) && !/\.(test|spec)\./.test(entry.name) && !entry.name.endsWith(".d.ts") ? [path] : [];
11
+ });
12
+ }
13
+ function assertErrorWiring(appDirectory) {
14
+ for (const entry of ["instrumentation.ts", "instrumentation-client.ts"]) {
15
+ const source = readFileSync(join(appDirectory, entry), "utf8");
16
+ assert.match(source, /(?:from\s*|import\s*)["']@weatherboard\/errors\//, `${entry} must import @weatherboard/errors`);
17
+ }
18
+ assert.ok(["src/app/global-error.tsx", "app/global-error.tsx", "src/app/global-error.jsx", "app/global-error.jsx"].some((path) => existsSync(join(appDirectory, path))), "a global-error boundary must exist");
19
+ for (const file of sources(appDirectory)) {
20
+ const source = readFileSync(file, "utf8");
21
+ const name = relative(appDirectory, file);
22
+ assert.doesNotMatch(source, /(?:from\s*|import\s*(?:\(\s*)?|require\s*\(\s*|export\s*[^;]*from\s*)["']@sentry\//, `${name}: products never import @sentry/* directly`);
23
+ assert.doesNotMatch(source, /\bposthog\s*(?:\.\s*captureException|\[\s*["']captureException["']\s*\])/, `${name}: PostHog is analytics only`);
24
+ assert.doesNotMatch(source, /["']?capture_exceptions["']?\s*:\s*true\b/, `${name}: PostHog exception autocapture must be off`);
25
+ }
26
+ }
27
+ function verifyErrors(appDirectory) {
28
+ test("shared error reporting wiring", () => assertErrorWiring(appDirectory));
29
+ }
30
+ export {
31
+ assertErrorWiring,
32
+ verifyErrors
33
+ };