@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 +112 -0
- package/lib/client.d.ts +17 -0
- package/lib/client.d.ts.map +1 -0
- package/lib/client.js +32 -0
- package/lib/client.js.map +1 -0
- package/lib/config.d.ts +17 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +60 -0
- package/lib/config.js.map +1 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +14 -0
- package/lib/index.js.map +1 -0
- package/lib/instrumentation.d.ts +3 -0
- package/lib/instrumentation.d.ts.map +1 -0
- package/lib/instrumentation.js +28 -0
- package/lib/instrumentation.js.map +1 -0
- package/lib/server.d.ts +48 -0
- package/lib/server.d.ts.map +1 -0
- package/lib/server.js +48 -0
- package/lib/server.js.map +1 -0
- package/package.json +72 -0
- package/src/__tests__/config.test.ts +61 -0
- package/src/__tests__/server.test.ts +47 -0
- package/src/client.ts +34 -0
- package/src/config.ts +76 -0
- package/src/index.ts +23 -0
- package/src/instrumentation.ts +28 -0
- package/src/server.ts +85 -0
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`
|
package/lib/client.d.ts
ADDED
|
@@ -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"}
|
package/lib/config.d.ts
ADDED
|
@@ -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
|
package/lib/index.js.map
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/lib/server.d.ts
ADDED
|
@@ -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
|
+
}
|