@goliapkg/sentori-next 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/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # @goliapkg/sentori-next
2
+
3
+ Next.js (App Router, ≥ 14) adapter for Sentori, built on
4
+ `@goliapkg/sentori-react` + `@goliapkg/sentori-javascript`.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ bun add @goliapkg/sentori-next
10
+ # or
11
+ pnpm add @goliapkg/sentori-next
12
+ ```
13
+
14
+ Set in `.env.local`:
15
+
16
+ ```
17
+ NEXT_PUBLIC_SENTORI_TOKEN=st_pk_...
18
+ NEXT_PUBLIC_SENTORI_RELEASE=myapp@1.2.3
19
+ NEXT_PUBLIC_SENTORI_ENVIRONMENT=prod
20
+
21
+ # Optional — server-only token / release if you want to differentiate
22
+ # server traffic from browser traffic on the dashboard.
23
+ SENTORI_TOKEN=st_pk_...
24
+ SENTORI_RELEASE=myapp-server@1.2.3
25
+ SENTORI_ENVIRONMENT=prod
26
+ ```
27
+
28
+ ## Wire it up
29
+
30
+ ### Server (instrumentation.ts at project root)
31
+
32
+ ```ts
33
+ // instrumentation.ts
34
+ export { register, onRequestError } from '@goliapkg/sentori-next/instrumentation'
35
+ ```
36
+
37
+ That's it — `register()` boots the SDK on Node start, and
38
+ `onRequestError` captures every server-side request error with route
39
+ + method tags. The `register()` helper guards on `NEXT_RUNTIME ===
40
+ 'nodejs'` so the edge runtime doesn't try to load Node-only deps.
41
+
42
+ ### Client (app/layout.tsx)
43
+
44
+ ```tsx
45
+ // app/layout.tsx
46
+ 'use client'
47
+ import { clientInit, SentoriProvider } from '@goliapkg/sentori-next/client'
48
+
49
+ clientInit() // reads NEXT_PUBLIC_SENTORI_*
50
+
51
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
52
+ return (
53
+ <html>
54
+ <body>
55
+ <SentoriProvider config={configFromEnv()}>{children}</SentoriProvider>
56
+ </body>
57
+ </html>
58
+ )
59
+ }
60
+ ```
61
+
62
+ ### App Router error boundary (app/error.tsx)
63
+
64
+ ```tsx
65
+ // app/error.tsx
66
+ 'use client'
67
+ import { SentoriErrorBoundary } from '@goliapkg/sentori-next/client'
68
+
69
+ export default function Error({ error, reset }: { error: Error; reset: () => void }) {
70
+ // Next already calls our boundary; here we render the fallback UI.
71
+ // The capture happened upstream via SentoriProvider's hook.
72
+ return (
73
+ <div>
74
+ <h2>Something went wrong</h2>
75
+ <button onClick={reset}>Try again</button>
76
+ </div>
77
+ )
78
+ }
79
+ ```
80
+
81
+ For a global catch-all, do the same in `app/global-error.tsx`.
82
+
83
+ ## What gets captured
84
+
85
+ | Path | Source tag |
86
+ |------|------------|
87
+ | Server route / API throw | `source=next.requestError`, `next.runtime=nodejs\|edge` |
88
+ | Component render error (App Router) | `source=react.errorBoundary` (via `<SentoriErrorBoundary>`) |
89
+ | Browser uncaught error / promise | `source=` (set by JS SDK hooks) |
90
+ | `useCaptureError(fn)` | per-call tags as you pass them |
91
+
92
+ ## Edge runtime
93
+
94
+ `onRequestError` works in both Node and Edge runtimes — Next forwards
95
+ the same signature. `serverInit()` is Node-only because Edge lacks
96
+ `process.on(...)` for `uncaughtException`.
97
+
98
+ ## Sub-paths
99
+
100
+ | Import | Use from |
101
+ |--------|----------|
102
+ | `@goliapkg/sentori-next/client` | App Router client components, `clientInit`, `SentoriProvider`, `<SentoriErrorBoundary>`, hooks |
103
+ | `@goliapkg/sentori-next/server` | `instrumentation.ts`, `serverInit`, `onRequestError` |
104
+ | `@goliapkg/sentori-next/instrumentation` | one-line `instrumentation.ts` re-export |
105
+
106
+ ## Versioning
107
+
108
+ Tracks the underlying SDKs:
109
+
110
+ - depends on `@goliapkg/sentori-react@0.1.0`
111
+ - depends on `@goliapkg/sentori-javascript@0.2.0`
112
+ - depends on `@goliapkg/sentori-core@0.1.0`
@@ -0,0 +1,17 @@
1
+ import { type SentoriNextConfig } from './config.js';
2
+ /**
3
+ * Initialise the JS SDK once on the browser. Idempotent across
4
+ * Next.js's React Refresh / fast-reload / route transitions.
5
+ *
6
+ * // app/layout.tsx
7
+ * 'use client'
8
+ * import { clientInit } from '@goliapkg/sentori-next/client'
9
+ * clientInit()
10
+ * export default function RootLayout({ children }) { ... }
11
+ *
12
+ * With NEXT_PUBLIC_SENTORI_TOKEN, NEXT_PUBLIC_SENTORI_RELEASE, and
13
+ * NEXT_PUBLIC_SENTORI_ENVIRONMENT set, no arguments are needed.
14
+ */
15
+ export declare function clientInit(cfg?: SentoriNextConfig): void;
16
+ export { SentoriProvider, SentoriErrorBoundary, useSentori, useCaptureError } from '@goliapkg/sentori-react';
17
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAInE;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,GAAG,GAAE,iBAAsB,GAAG,IAAI,CAS5D;AAED,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA"}
package/lib/client.js ADDED
@@ -0,0 +1,32 @@
1
+ // Browser-side Next entry point. Used from a Next "use client" file
2
+ // in app/layout.tsx — pairs with serverInit() in instrumentation.ts.
3
+ import { initSentori } from '@goliapkg/sentori-javascript';
4
+ import { resolveConfig } from './config.js';
5
+ let _initialised = false;
6
+ /**
7
+ * Initialise the JS SDK once on the browser. Idempotent across
8
+ * Next.js's React Refresh / fast-reload / route transitions.
9
+ *
10
+ * // app/layout.tsx
11
+ * 'use client'
12
+ * import { clientInit } from '@goliapkg/sentori-next/client'
13
+ * clientInit()
14
+ * export default function RootLayout({ children }) { ... }
15
+ *
16
+ * With NEXT_PUBLIC_SENTORI_TOKEN, NEXT_PUBLIC_SENTORI_RELEASE, and
17
+ * NEXT_PUBLIC_SENTORI_ENVIRONMENT set, no arguments are needed.
18
+ */
19
+ export function clientInit(cfg = {}) {
20
+ if (_initialised)
21
+ return;
22
+ try {
23
+ initSentori(resolveConfig('client', cfg));
24
+ _initialised = true;
25
+ }
26
+ catch (e) {
27
+ // eslint-disable-next-line no-console
28
+ console.error('[sentori-next] client init failed', e);
29
+ }
30
+ }
31
+ export { SentoriProvider, SentoriErrorBoundary, useSentori, useCaptureError } from '@goliapkg/sentori-react';
32
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,qEAAqE;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAE1D,OAAO,EAAE,aAAa,EAA0B,MAAM,aAAa,CAAA;AAEnE,IAAI,YAAY,GAAG,KAAK,CAAA;AAExB;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CAAC,MAAyB,EAAE;IACpD,IAAI,YAAY;QAAE,OAAM;IACxB,IAAI,CAAC;QACH,WAAW,CAAC,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAA;QACzC,YAAY,GAAG,IAAI,CAAA;IACrB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,sCAAsC;QACtC,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,CAAC,CAAC,CAAA;IACvD,CAAC;AACH,CAAC;AAED,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA"}
@@ -0,0 +1,17 @@
1
+ import type { CommonInitOptions } from '@goliapkg/sentori-core';
2
+ export type Side = 'client' | 'server';
3
+ export type SentoriNextConfig = Partial<CommonInitOptions> & {
4
+ /** Override the env-resolution. Useful in tests. */
5
+ envOverride?: Record<string, string | undefined>;
6
+ };
7
+ /**
8
+ * Resolve a complete CommonInitOptions from env + explicit overrides.
9
+ * `side` controls the env prefix; explicit values from `cfg` always
10
+ * win.
11
+ *
12
+ * Throws when a required field is unresolved on either side — the
13
+ * caller can catch + log at boot time and continue without Sentori
14
+ * if the env isn't wired yet.
15
+ */
16
+ export declare function resolveConfig(side: Side, cfg?: SentoriNextConfig): CommonInitOptions;
17
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAE/D,MAAM,MAAM,IAAI,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEtC,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,GAAG;IAC3D,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CACjD,CAAA;AAYD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,GAAE,iBAAsB,GAAG,iBAAiB,CA+BxF"}
package/lib/config.js ADDED
@@ -0,0 +1,60 @@
1
+ // Phase 21 sub-C: env-driven config resolution.
2
+ //
3
+ // Next.js convention is `NEXT_PUBLIC_*` for browser-readable values
4
+ // and unprefixed for server-only. We honour both — clientInit() reads
5
+ // NEXT_PUBLIC_SENTORI_* (the bundler inlines these at build time);
6
+ // serverInit() reads SENTORI_* first, falling back to NEXT_PUBLIC_*
7
+ // so a single SaaS deploy can share a token between server and client
8
+ // when that's desired.
9
+ const CLIENT_PREFIX = 'NEXT_PUBLIC_SENTORI_';
10
+ const SERVER_PREFIX = 'SENTORI_';
11
+ const KEY_MAP = {
12
+ environment: 'ENVIRONMENT',
13
+ ingestUrl: 'INGEST_URL',
14
+ release: 'RELEASE',
15
+ token: 'TOKEN',
16
+ };
17
+ /**
18
+ * Resolve a complete CommonInitOptions from env + explicit overrides.
19
+ * `side` controls the env prefix; explicit values from `cfg` always
20
+ * win.
21
+ *
22
+ * Throws when a required field is unresolved on either side — the
23
+ * caller can catch + log at boot time and continue without Sentori
24
+ * if the env isn't wired yet.
25
+ */
26
+ export function resolveConfig(side, cfg = {}) {
27
+ const env = cfg.envOverride ?? processEnv();
28
+ const out = {};
29
+ for (const k of Object.keys(KEY_MAP)) {
30
+ const explicit = cfg[k];
31
+ if (explicit !== undefined) {
32
+ out[k] = explicit;
33
+ continue;
34
+ }
35
+ const suffix = KEY_MAP[k];
36
+ const browser = env[`${CLIENT_PREFIX}${suffix}`];
37
+ const server = env[`${SERVER_PREFIX}${suffix}`];
38
+ const v = side === 'client' ? browser : (server ?? browser);
39
+ if (v)
40
+ out[k] = v;
41
+ }
42
+ // Defaults: ingestUrl points at the public SaaS if nothing was set.
43
+ if (!out.ingestUrl)
44
+ out.ingestUrl = 'https://ingest.sentori.golia.jp';
45
+ for (const required of ['environment', 'release', 'token']) {
46
+ if (!out[required]) {
47
+ throw new Error(`[sentori-next] missing config field "${required}" (set ` +
48
+ `${side === 'client' ? CLIENT_PREFIX : SERVER_PREFIX}${KEY_MAP[required]} ` +
49
+ `or pass it explicitly)`);
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+ function processEnv() {
55
+ // Both Node and browser bundlers expose `process.env` after Next's
56
+ // build pipeline. The browser version only contains NEXT_PUBLIC_*.
57
+ const p = globalThis.process;
58
+ return p?.env ?? {};
59
+ }
60
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,oEAAoE;AACpE,sEAAsE;AACtE,mEAAmE;AACnE,oEAAoE;AACpE,sEAAsE;AACtE,uBAAuB;AAWvB,MAAM,aAAa,GAAG,sBAAsB,CAAA;AAC5C,MAAM,aAAa,GAAG,UAAU,CAAA;AAEhC,MAAM,OAAO,GAA4C;IACvD,WAAW,EAAE,aAAa;IAC1B,SAAS,EAAE,YAAY;IACvB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,OAAO;CACf,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,IAAU,EAAE,MAAyB,EAAE;IACnE,MAAM,GAAG,GAAG,GAAG,CAAC,WAAW,IAAI,UAAU,EAAE,CAAA;IAC3C,MAAM,GAAG,GAA+B,EAAE,CAAA;IAE1C,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAgC,EAAE,CAAC;QACpE,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;QACvB,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAA;YACjB,SAAQ;QACV,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;QACzB,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,aAAa,GAAG,MAAM,EAAE,CAAC,CAAA;QAChD,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,aAAa,GAAG,MAAM,EAAE,CAAC,CAAA;QAC/C,MAAM,CAAC,GAAG,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,CAAA;QAC3D,IAAI,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACnB,CAAC;IAED,oEAAoE;IACpE,IAAI,CAAC,GAAG,CAAC,SAAS;QAAE,GAAG,CAAC,SAAS,GAAG,iCAAiC,CAAA;IAErE,KAAK,MAAM,QAAQ,IAAI,CAAC,aAAa,EAAE,SAAS,EAAE,OAAO,CAAU,EAAE,CAAC;QACpE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CACb,wCAAwC,QAAQ,SAAS;gBACvD,GAAG,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAC3E,wBAAwB,CAC3B,CAAA;QACH,CAAC;IACH,CAAC;IAED,OAAO,GAAwB,CAAA;AACjC,CAAC;AAED,SAAS,UAAU;IACjB,mEAAmE;IACnE,mEAAmE;IACnE,MAAM,CAAC,GAAI,UAAyE,CAAC,OAAO,CAAA;IAC5F,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,CAAA;AACrB,CAAC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { clientInit } from './client.js';
2
+ export { serverInit, onRequestError } from './server.js';
3
+ export { resolveConfig } from './config.js';
4
+ export type { SentoriNextConfig } from './config.js';
5
+ export type { RequestErrorContext, RequestErrorRequest } from './server.js';
6
+ export { SentoriErrorBoundary, SentoriProvider, useCaptureError, useSentori, } from '@goliapkg/sentori-react';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAE3E,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,UAAU,GACX,MAAM,yBAAyB,CAAA"}
package/lib/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // Top-level entry point. Most callers should pull from the more
2
+ // specific subpaths instead — see exports map in package.json:
3
+ //
4
+ // @goliapkg/sentori-next/client — clientInit + React surface
5
+ // @goliapkg/sentori-next/server — serverInit + onRequestError
6
+ // @goliapkg/sentori-next/instrumentation — drop-in register/onRequestError
7
+ //
8
+ // Re-exports below are kept thin so a default `import { ... } from
9
+ // '@goliapkg/sentori-next'` still works for the common cases.
10
+ export { clientInit } from './client.js';
11
+ export { serverInit, onRequestError } from './server.js';
12
+ export { resolveConfig } from './config.js';
13
+ export { SentoriErrorBoundary, SentoriProvider, useCaptureError, useSentori, } from '@goliapkg/sentori-react';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,+DAA+D;AAC/D,EAAE;AACF,wEAAwE;AACxE,yEAAyE;AACzE,6EAA6E;AAC7E,EAAE;AACF,mEAAmE;AACnE,8DAA8D;AAE9D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAK3C,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,UAAU,GACX,MAAM,yBAAyB,CAAA"}
@@ -0,0 +1,3 @@
1
+ export declare function register(): Promise<void>;
2
+ export { onRequestError } from './server.js';
3
+ //# sourceMappingURL=instrumentation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrumentation.d.ts","sourceRoot":"","sources":["../src/instrumentation.ts"],"names":[],"mappings":"AAmBA,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAM9C;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,28 @@
1
+ // Convenience re-export so users can drop a one-liner into their
2
+ // instrumentation.ts:
3
+ //
4
+ // // instrumentation.ts
5
+ // export { register, onRequestError } from '@goliapkg/sentori-next/instrumentation'
6
+ //
7
+ // Equivalent to writing the longer form by hand:
8
+ //
9
+ // export async function register() {
10
+ // if (process.env.NEXT_RUNTIME === 'nodejs') {
11
+ // const { serverInit } = await import('@goliapkg/sentori-next/server')
12
+ // serverInit()
13
+ // }
14
+ // }
15
+ // export { onRequestError } from '@goliapkg/sentori-next/server'
16
+ //
17
+ // The dynamic import keeps Next's edge runtime build from pulling in
18
+ // Node-only deps when NEXT_RUNTIME === 'edge'.
19
+ export async function register() {
20
+ const env = globalThis.process
21
+ ?.env;
22
+ if (env?.NEXT_RUNTIME !== 'nodejs')
23
+ return;
24
+ const { serverInit } = await import('./server.js');
25
+ serverInit();
26
+ }
27
+ export { onRequestError } from './server.js';
28
+ //# sourceMappingURL=instrumentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrumentation.js","sourceRoot":"","sources":["../src/instrumentation.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,sBAAsB;AACtB,EAAE;AACF,4BAA4B;AAC5B,wFAAwF;AACxF,EAAE;AACF,iDAAiD;AACjD,EAAE;AACF,yCAAyC;AACzC,qDAAqD;AACrD,+EAA+E;AAC/E,uBAAuB;AACvB,UAAU;AACV,QAAQ;AACR,qEAAqE;AACrE,EAAE;AACF,qEAAqE;AACrE,+CAA+C;AAE/C,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC5B,MAAM,GAAG,GAAI,UAAyE,CAAC,OAAO;QAC5F,EAAE,GAAG,CAAA;IACP,IAAI,GAAG,EAAE,YAAY,KAAK,QAAQ;QAAE,OAAM;IAC1C,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAA;IAClD,UAAU,EAAE,CAAA;AACd,CAAC;AAED,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,48 @@
1
+ import { type SentoriNextConfig } from './config.js';
2
+ /**
3
+ * Initialise the JS SDK on the Node server. Called from
4
+ * instrumentation.ts:
5
+ *
6
+ * // instrumentation.ts
7
+ * export async function register() {
8
+ * if (process.env.NEXT_RUNTIME === 'nodejs') {
9
+ * const { serverInit } = await import('@goliapkg/sentori-next/server')
10
+ * serverInit()
11
+ * }
12
+ * }
13
+ *
14
+ * Edge runtime is intentionally not initialised here — Next's edge
15
+ * environment lacks `process` and the Node-only Node hooks would
16
+ * throw. Edge errors flow through `onRequestError` below.
17
+ */
18
+ export declare function serverInit(cfg?: SentoriNextConfig): void;
19
+ /**
20
+ * Next's instrumentation.ts:onRequestError signature, wired to the
21
+ * SDK's captureError. Tags the event with the route + HTTP method
22
+ * + the runtime that caught it ("nodejs" | "edge").
23
+ *
24
+ * // instrumentation.ts
25
+ * export { onRequestError } from '@goliapkg/sentori-next/server'
26
+ *
27
+ * Or compose:
28
+ *
29
+ * export async function onRequestError(err, request, context) {
30
+ * const { onRequestError } = await import('@goliapkg/sentori-next/server')
31
+ * await onRequestError(err, request, context)
32
+ * // your own logging
33
+ * }
34
+ */
35
+ export type RequestErrorContext = {
36
+ routePath?: string;
37
+ routeType?: 'app' | 'pages' | 'route';
38
+ routerKind?: 'App Router' | 'Pages Router';
39
+ runtime?: 'edge' | 'nodejs';
40
+ };
41
+ export type RequestErrorRequest = {
42
+ headers?: Record<string, string | string[] | undefined>;
43
+ method?: string;
44
+ path?: string;
45
+ url?: string;
46
+ };
47
+ export declare function onRequestError(err: Error | unknown, request: RequestErrorRequest, context?: RequestErrorContext): Promise<void>;
48
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAOA,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAInE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CAAC,GAAG,GAAE,iBAAsB,GAAG,IAAI,CAS5D;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,OAAO,CAAA;IACrC,UAAU,CAAC,EAAE,YAAY,GAAG,cAAc,CAAA;IAE1C,OAAO,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;IACvD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;CACb,CAAA;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,KAAK,GAAG,OAAO,EACpB,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAWf"}
package/lib/server.js ADDED
@@ -0,0 +1,48 @@
1
+ // Server-side Next entry point. Used from instrumentation.ts'
2
+ // register() function. The JS SDK's Node hooks (uncaughtException +
3
+ // unhandledRejection) are wired here; route-handler errors are
4
+ // captured via the onRequestError export below.
5
+ import { captureError, initSentori } from '@goliapkg/sentori-javascript';
6
+ import { resolveConfig } from './config.js';
7
+ let _initialised = false;
8
+ /**
9
+ * Initialise the JS SDK on the Node server. Called from
10
+ * instrumentation.ts:
11
+ *
12
+ * // instrumentation.ts
13
+ * export async function register() {
14
+ * if (process.env.NEXT_RUNTIME === 'nodejs') {
15
+ * const { serverInit } = await import('@goliapkg/sentori-next/server')
16
+ * serverInit()
17
+ * }
18
+ * }
19
+ *
20
+ * Edge runtime is intentionally not initialised here — Next's edge
21
+ * environment lacks `process` and the Node-only Node hooks would
22
+ * throw. Edge errors flow through `onRequestError` below.
23
+ */
24
+ export function serverInit(cfg = {}) {
25
+ if (_initialised)
26
+ return;
27
+ try {
28
+ initSentori(resolveConfig('server', cfg));
29
+ _initialised = true;
30
+ }
31
+ catch (e) {
32
+ // eslint-disable-next-line no-console
33
+ console.error('[sentori-next] server init failed', e);
34
+ }
35
+ }
36
+ export async function onRequestError(err, request, context) {
37
+ const error = err instanceof Error ? err : new Error(String(err));
38
+ captureError(error, {
39
+ tags: {
40
+ 'next.method': request?.method ?? '',
41
+ 'next.route': context?.routePath ?? request?.path ?? request?.url ?? '',
42
+ 'next.routeType': context?.routeType ?? '',
43
+ 'next.runtime': context?.runtime ?? 'unknown',
44
+ source: 'next.requestError',
45
+ },
46
+ });
47
+ }
48
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,oEAAoE;AACpE,+DAA+D;AAC/D,gDAAgD;AAEhD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAExE,OAAO,EAAE,aAAa,EAA0B,MAAM,aAAa,CAAA;AAEnE,IAAI,YAAY,GAAG,KAAK,CAAA;AAExB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,UAAU,CAAC,MAAyB,EAAE;IACpD,IAAI,YAAY;QAAE,OAAM;IACxB,IAAI,CAAC;QACH,WAAW,CAAC,aAAa,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAA;QACzC,YAAY,GAAG,IAAI,CAAA;IACrB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,sCAAsC;QACtC,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,CAAC,CAAC,CAAA;IACvD,CAAC;AACH,CAAC;AAiCD,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAoB,EACpB,OAA4B,EAC5B,OAA6B;IAE7B,MAAM,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;IACjE,YAAY,CAAC,KAAK,EAAE;QAClB,IAAI,EAAE;YACJ,aAAa,EAAE,OAAO,EAAE,MAAM,IAAI,EAAE;YACpC,YAAY,EAAE,OAAO,EAAE,SAAS,IAAI,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,GAAG,IAAI,EAAE;YACvE,gBAAgB,EAAE,OAAO,EAAE,SAAS,IAAI,EAAE;YAC1C,cAAc,EAAE,OAAO,EAAE,OAAO,IAAI,SAAS;YAC7C,MAAM,EAAE,mBAAmB;SAC5B;KACF,CAAC,CAAA;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@goliapkg/sentori-next",
3
+ "version": "0.1.0",
4
+ "description": "Next.js adapter for Sentori — instrumentation.ts hooks, App Router error boundary, env-driven provider built on @goliapkg/sentori-react.",
5
+ "license": "MIT",
6
+ "homepage": "https://sentori.golia.jp",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/goliajp/sentori.git",
10
+ "directory": "sdk/next"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/goliajp/sentori/issues"
14
+ },
15
+ "keywords": [
16
+ "sentori",
17
+ "error-tracking",
18
+ "next",
19
+ "nextjs",
20
+ "app-router"
21
+ ],
22
+ "type": "module",
23
+ "main": "./lib/index.js",
24
+ "types": "./lib/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./lib/index.d.ts",
28
+ "default": "./lib/index.js"
29
+ },
30
+ "./client": {
31
+ "types": "./lib/client.d.ts",
32
+ "default": "./lib/client.js"
33
+ },
34
+ "./server": {
35
+ "types": "./lib/server.d.ts",
36
+ "default": "./lib/server.js"
37
+ },
38
+ "./instrumentation": {
39
+ "types": "./lib/instrumentation.d.ts",
40
+ "default": "./lib/instrumentation.js"
41
+ }
42
+ },
43
+ "files": [
44
+ "lib/",
45
+ "src/",
46
+ "README.md"
47
+ ],
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.json",
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "bun test",
52
+ "prepack": "bun run build"
53
+ },
54
+ "peerDependencies": {
55
+ "next": ">=14",
56
+ "react": ">=18"
57
+ },
58
+ "dependencies": {
59
+ "@goliapkg/sentori-core": "0.1.0",
60
+ "@goliapkg/sentori-javascript": "0.2.0",
61
+ "@goliapkg/sentori-react": "0.1.0"
62
+ },
63
+ "devDependencies": {
64
+ "@types/bun": "latest",
65
+ "@types/react": "^19",
66
+ "react": "^19",
67
+ "typescript": "^5"
68
+ },
69
+ "publishConfig": {
70
+ "access": "public"
71
+ }
72
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import { resolveConfig } from '../config.js'
4
+
5
+ const FULL_CLIENT = {
6
+ NEXT_PUBLIC_SENTORI_ENVIRONMENT: 'prod',
7
+ NEXT_PUBLIC_SENTORI_RELEASE: 'app@1.2.3',
8
+ NEXT_PUBLIC_SENTORI_TOKEN: 'st_pk_testtesttesttesttesttesttest',
9
+ }
10
+
11
+ const FULL_SERVER = {
12
+ SENTORI_ENVIRONMENT: 'prod',
13
+ SENTORI_INGEST_URL: 'http://localhost:8080',
14
+ SENTORI_RELEASE: 'app@1.2.3',
15
+ SENTORI_TOKEN: 'st_pk_serverservertestservertest',
16
+ }
17
+
18
+ describe('resolveConfig', () => {
19
+ test('client: pulls NEXT_PUBLIC_* and falls back to public ingest', () => {
20
+ const cfg = resolveConfig('client', { envOverride: FULL_CLIENT })
21
+ expect(cfg.environment).toBe('prod')
22
+ expect(cfg.release).toBe('app@1.2.3')
23
+ expect(cfg.token).toBe('st_pk_testtesttesttesttesttesttest')
24
+ expect(cfg.ingestUrl).toBe('https://ingest.sentori.golia.jp')
25
+ })
26
+
27
+ test('server: prefers SENTORI_* over NEXT_PUBLIC_*', () => {
28
+ const env = {
29
+ ...FULL_SERVER,
30
+ NEXT_PUBLIC_SENTORI_TOKEN: 'public-loses',
31
+ }
32
+ const cfg = resolveConfig('server', { envOverride: env })
33
+ expect(cfg.token).toBe('st_pk_serverservertestservertest')
34
+ expect(cfg.ingestUrl).toBe('http://localhost:8080')
35
+ })
36
+
37
+ test('server: falls back to NEXT_PUBLIC_* when SENTORI_* missing', () => {
38
+ const cfg = resolveConfig('server', { envOverride: FULL_CLIENT })
39
+ expect(cfg.token).toBe('st_pk_testtesttesttesttesttesttest')
40
+ })
41
+
42
+ test('explicit overrides win over env', () => {
43
+ const cfg = resolveConfig('client', {
44
+ envOverride: FULL_CLIENT,
45
+ release: 'overridden@9.9.9',
46
+ })
47
+ expect(cfg.release).toBe('overridden@9.9.9')
48
+ })
49
+
50
+ test('throws on missing required field', () => {
51
+ expect(() => resolveConfig('client', { envOverride: {} })).toThrow(
52
+ /missing config field "environment"/,
53
+ )
54
+ })
55
+
56
+ test('client side does not see SENTORI_* (only NEXT_PUBLIC_*)', () => {
57
+ expect(() => resolveConfig('client', { envOverride: FULL_SERVER })).toThrow(
58
+ /missing config field/,
59
+ )
60
+ })
61
+ })
@@ -0,0 +1,47 @@
1
+ import { describe, expect, mock, test } from 'bun:test'
2
+
3
+ // Mock the JS SDK's captureError so we observe what onRequestError
4
+ // would have sent without spinning up a real transport.
5
+ const captured: { error: Error; tags: Record<string, string> }[] = []
6
+
7
+ mock.module('@goliapkg/sentori-javascript', () => ({
8
+ captureError: (error: Error, extras: { tags: Record<string, string> }) => {
9
+ captured.push({ error, tags: extras.tags })
10
+ },
11
+ // serverInit only uses initSentori; mock it to a noop.
12
+ initSentori: () => {},
13
+ }))
14
+
15
+ const { onRequestError } = await import('../server.js')
16
+
17
+ describe('onRequestError', () => {
18
+ test('captures Error subclasses', async () => {
19
+ captured.length = 0
20
+ await onRequestError(
21
+ new TypeError('boom'),
22
+ { method: 'GET', path: '/api/widgets' },
23
+ { routePath: '/api/widgets', routeType: 'route', runtime: 'nodejs' },
24
+ )
25
+ expect(captured).toHaveLength(1)
26
+ expect(captured[0]!.error.message).toBe('boom')
27
+ expect(captured[0]!.tags).toMatchObject({
28
+ 'next.method': 'GET',
29
+ 'next.route': '/api/widgets',
30
+ 'next.runtime': 'nodejs',
31
+ source: 'next.requestError',
32
+ })
33
+ })
34
+
35
+ test('wraps non-Error throws into Error', async () => {
36
+ captured.length = 0
37
+ await onRequestError('string error', { method: 'POST' })
38
+ expect(captured).toHaveLength(1)
39
+ expect(captured[0]!.error.message).toBe('string error')
40
+ })
41
+
42
+ test('falls back to request.path / request.url when context.routePath is absent', async () => {
43
+ captured.length = 0
44
+ await onRequestError(new Error('x'), { method: 'GET', url: '/from-url' })
45
+ expect(captured[0]!.tags['next.route']).toBe('/from-url')
46
+ })
47
+ })
package/src/client.ts ADDED
@@ -0,0 +1,34 @@
1
+ // Browser-side Next entry point. Used from a Next "use client" file
2
+ // in app/layout.tsx — pairs with serverInit() in instrumentation.ts.
3
+
4
+ import { initSentori } from '@goliapkg/sentori-javascript'
5
+
6
+ import { resolveConfig, type SentoriNextConfig } from './config.js'
7
+
8
+ let _initialised = false
9
+
10
+ /**
11
+ * Initialise the JS SDK once on the browser. Idempotent across
12
+ * Next.js's React Refresh / fast-reload / route transitions.
13
+ *
14
+ * // app/layout.tsx
15
+ * 'use client'
16
+ * import { clientInit } from '@goliapkg/sentori-next/client'
17
+ * clientInit()
18
+ * export default function RootLayout({ children }) { ... }
19
+ *
20
+ * With NEXT_PUBLIC_SENTORI_TOKEN, NEXT_PUBLIC_SENTORI_RELEASE, and
21
+ * NEXT_PUBLIC_SENTORI_ENVIRONMENT set, no arguments are needed.
22
+ */
23
+ export function clientInit(cfg: SentoriNextConfig = {}): void {
24
+ if (_initialised) return
25
+ try {
26
+ initSentori(resolveConfig('client', cfg))
27
+ _initialised = true
28
+ } catch (e) {
29
+ // eslint-disable-next-line no-console
30
+ console.error('[sentori-next] client init failed', e)
31
+ }
32
+ }
33
+
34
+ export { SentoriProvider, SentoriErrorBoundary, useSentori, useCaptureError } from '@goliapkg/sentori-react'
package/src/config.ts ADDED
@@ -0,0 +1,76 @@
1
+ // Phase 21 sub-C: env-driven config resolution.
2
+ //
3
+ // Next.js convention is `NEXT_PUBLIC_*` for browser-readable values
4
+ // and unprefixed for server-only. We honour both — clientInit() reads
5
+ // NEXT_PUBLIC_SENTORI_* (the bundler inlines these at build time);
6
+ // serverInit() reads SENTORI_* first, falling back to NEXT_PUBLIC_*
7
+ // so a single SaaS deploy can share a token between server and client
8
+ // when that's desired.
9
+
10
+ import type { CommonInitOptions } from '@goliapkg/sentori-core'
11
+
12
+ export type Side = 'client' | 'server'
13
+
14
+ export type SentoriNextConfig = Partial<CommonInitOptions> & {
15
+ /** Override the env-resolution. Useful in tests. */
16
+ envOverride?: Record<string, string | undefined>
17
+ }
18
+
19
+ const CLIENT_PREFIX = 'NEXT_PUBLIC_SENTORI_'
20
+ const SERVER_PREFIX = 'SENTORI_'
21
+
22
+ const KEY_MAP: Record<keyof CommonInitOptions, string> = {
23
+ environment: 'ENVIRONMENT',
24
+ ingestUrl: 'INGEST_URL',
25
+ release: 'RELEASE',
26
+ token: 'TOKEN',
27
+ }
28
+
29
+ /**
30
+ * Resolve a complete CommonInitOptions from env + explicit overrides.
31
+ * `side` controls the env prefix; explicit values from `cfg` always
32
+ * win.
33
+ *
34
+ * Throws when a required field is unresolved on either side — the
35
+ * caller can catch + log at boot time and continue without Sentori
36
+ * if the env isn't wired yet.
37
+ */
38
+ export function resolveConfig(side: Side, cfg: SentoriNextConfig = {}): CommonInitOptions {
39
+ const env = cfg.envOverride ?? processEnv()
40
+ const out: Partial<CommonInitOptions> = {}
41
+
42
+ for (const k of Object.keys(KEY_MAP) as (keyof CommonInitOptions)[]) {
43
+ const explicit = cfg[k]
44
+ if (explicit !== undefined) {
45
+ out[k] = explicit
46
+ continue
47
+ }
48
+ const suffix = KEY_MAP[k]
49
+ const browser = env[`${CLIENT_PREFIX}${suffix}`]
50
+ const server = env[`${SERVER_PREFIX}${suffix}`]
51
+ const v = side === 'client' ? browser : (server ?? browser)
52
+ if (v) out[k] = v
53
+ }
54
+
55
+ // Defaults: ingestUrl points at the public SaaS if nothing was set.
56
+ if (!out.ingestUrl) out.ingestUrl = 'https://ingest.sentori.golia.jp'
57
+
58
+ for (const required of ['environment', 'release', 'token'] as const) {
59
+ if (!out[required]) {
60
+ throw new Error(
61
+ `[sentori-next] missing config field "${required}" (set ` +
62
+ `${side === 'client' ? CLIENT_PREFIX : SERVER_PREFIX}${KEY_MAP[required]} ` +
63
+ `or pass it explicitly)`,
64
+ )
65
+ }
66
+ }
67
+
68
+ return out as CommonInitOptions
69
+ }
70
+
71
+ function processEnv(): Record<string, string | undefined> {
72
+ // Both Node and browser bundlers expose `process.env` after Next's
73
+ // build pipeline. The browser version only contains NEXT_PUBLIC_*.
74
+ const p = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
75
+ return p?.env ?? {}
76
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ // Top-level entry point. Most callers should pull from the more
2
+ // specific subpaths instead — see exports map in package.json:
3
+ //
4
+ // @goliapkg/sentori-next/client — clientInit + React surface
5
+ // @goliapkg/sentori-next/server — serverInit + onRequestError
6
+ // @goliapkg/sentori-next/instrumentation — drop-in register/onRequestError
7
+ //
8
+ // Re-exports below are kept thin so a default `import { ... } from
9
+ // '@goliapkg/sentori-next'` still works for the common cases.
10
+
11
+ export { clientInit } from './client.js'
12
+ export { serverInit, onRequestError } from './server.js'
13
+ export { resolveConfig } from './config.js'
14
+
15
+ export type { SentoriNextConfig } from './config.js'
16
+ export type { RequestErrorContext, RequestErrorRequest } from './server.js'
17
+
18
+ export {
19
+ SentoriErrorBoundary,
20
+ SentoriProvider,
21
+ useCaptureError,
22
+ useSentori,
23
+ } from '@goliapkg/sentori-react'
@@ -0,0 +1,28 @@
1
+ // Convenience re-export so users can drop a one-liner into their
2
+ // instrumentation.ts:
3
+ //
4
+ // // instrumentation.ts
5
+ // export { register, onRequestError } from '@goliapkg/sentori-next/instrumentation'
6
+ //
7
+ // Equivalent to writing the longer form by hand:
8
+ //
9
+ // export async function register() {
10
+ // if (process.env.NEXT_RUNTIME === 'nodejs') {
11
+ // const { serverInit } = await import('@goliapkg/sentori-next/server')
12
+ // serverInit()
13
+ // }
14
+ // }
15
+ // export { onRequestError } from '@goliapkg/sentori-next/server'
16
+ //
17
+ // The dynamic import keeps Next's edge runtime build from pulling in
18
+ // Node-only deps when NEXT_RUNTIME === 'edge'.
19
+
20
+ export async function register(): Promise<void> {
21
+ const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
22
+ ?.env
23
+ if (env?.NEXT_RUNTIME !== 'nodejs') return
24
+ const { serverInit } = await import('./server.js')
25
+ serverInit()
26
+ }
27
+
28
+ export { onRequestError } from './server.js'
package/src/server.ts ADDED
@@ -0,0 +1,85 @@
1
+ // Server-side Next entry point. Used from instrumentation.ts'
2
+ // register() function. The JS SDK's Node hooks (uncaughtException +
3
+ // unhandledRejection) are wired here; route-handler errors are
4
+ // captured via the onRequestError export below.
5
+
6
+ import { captureError, initSentori } from '@goliapkg/sentori-javascript'
7
+
8
+ import { resolveConfig, type SentoriNextConfig } from './config.js'
9
+
10
+ let _initialised = false
11
+
12
+ /**
13
+ * Initialise the JS SDK on the Node server. Called from
14
+ * instrumentation.ts:
15
+ *
16
+ * // instrumentation.ts
17
+ * export async function register() {
18
+ * if (process.env.NEXT_RUNTIME === 'nodejs') {
19
+ * const { serverInit } = await import('@goliapkg/sentori-next/server')
20
+ * serverInit()
21
+ * }
22
+ * }
23
+ *
24
+ * Edge runtime is intentionally not initialised here — Next's edge
25
+ * environment lacks `process` and the Node-only Node hooks would
26
+ * throw. Edge errors flow through `onRequestError` below.
27
+ */
28
+ export function serverInit(cfg: SentoriNextConfig = {}): void {
29
+ if (_initialised) return
30
+ try {
31
+ initSentori(resolveConfig('server', cfg))
32
+ _initialised = true
33
+ } catch (e) {
34
+ // eslint-disable-next-line no-console
35
+ console.error('[sentori-next] server init failed', e)
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Next's instrumentation.ts:onRequestError signature, wired to the
41
+ * SDK's captureError. Tags the event with the route + HTTP method
42
+ * + the runtime that caught it ("nodejs" | "edge").
43
+ *
44
+ * // instrumentation.ts
45
+ * export { onRequestError } from '@goliapkg/sentori-next/server'
46
+ *
47
+ * Or compose:
48
+ *
49
+ * export async function onRequestError(err, request, context) {
50
+ * const { onRequestError } = await import('@goliapkg/sentori-next/server')
51
+ * await onRequestError(err, request, context)
52
+ * // your own logging
53
+ * }
54
+ */
55
+ export type RequestErrorContext = {
56
+ routePath?: string
57
+ routeType?: 'app' | 'pages' | 'route'
58
+ routerKind?: 'App Router' | 'Pages Router'
59
+ // Next 15+ adds runtime here; older versions leave it undefined.
60
+ runtime?: 'edge' | 'nodejs'
61
+ }
62
+
63
+ export type RequestErrorRequest = {
64
+ headers?: Record<string, string | string[] | undefined>
65
+ method?: string
66
+ path?: string
67
+ url?: string
68
+ }
69
+
70
+ export async function onRequestError(
71
+ err: Error | unknown,
72
+ request: RequestErrorRequest,
73
+ context?: RequestErrorContext,
74
+ ): Promise<void> {
75
+ const error = err instanceof Error ? err : new Error(String(err))
76
+ captureError(error, {
77
+ tags: {
78
+ 'next.method': request?.method ?? '',
79
+ 'next.route': context?.routePath ?? request?.path ?? request?.url ?? '',
80
+ 'next.routeType': context?.routeType ?? '',
81
+ 'next.runtime': context?.runtime ?? 'unknown',
82
+ source: 'next.requestError',
83
+ },
84
+ })
85
+ }