@worknice/instrumentation 1.0.1

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,16 @@
1
+ Copyright (c) 2024 Worknice
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
4
+ associated documentation files (the "Software"), to deal in the Software without restriction,
5
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
6
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
7
+ furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial
10
+ portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
13
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
14
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
15
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
16
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # @worknice/instrumentation
2
+
3
+ Shared instrumentation and correlation ID management for Worknice applications.
4
+
5
+ ## Features
6
+
7
+ - 🔍 **Correlation ID Tracking**: Request tracing across async boundaries using Node.js AsyncLocalStorage
8
+ - 📊 **Next.js Instrumentation**: Error reporting with Axiom
9
+ - 🔄 **Cross-Service Tracing**: Track requests across services (Notebook, MYOB, Micropay, Basics)
10
+
11
+ ## Installation
12
+
13
+ This package is part of the Worknice monorepo and is automatically available to all apps:
14
+
15
+ ```bash
16
+ pnpm add @worknice/instrumentation
17
+ ```
18
+
19
+ ## Getting Started
20
+
21
+ ### App Instrumentation
22
+
23
+ For Next.js apps, create an `instrumentation.ts` file in the app root:
24
+
25
+ ```typescript
26
+ // apps/$app-name/instrumentation.ts
27
+ export { register, onRequestError } from "@worknice/instrumentation";
28
+ ```
29
+
30
+ - `register()` - Initialises instrumentation. Automatically called by Next.js when instrumentation is enabled.
31
+ - `onRequestError()` - Handles and logs request errors with correlation tracking. Automatically called by Next.js on errors.
32
+
33
+ ### Correlation ID Usage
34
+
35
+ The package provides multiple ways to work with correlation IDs:
36
+
37
+ #### Getting the current correlation ID
38
+
39
+ ```typescript
40
+ import { getCorrelationId } from "@worknice/instrumentation";
41
+
42
+ const correlationId = getCorrelationId();
43
+ ```
44
+
45
+ #### Running code with a specific correlation ID
46
+
47
+ ```typescript
48
+ import { runWithCorrelationId, getCorrelationId } from "@worknice/instrumentation";
49
+
50
+ await runWithCorrelationId("$correlationId", async () => {
51
+ // All async operations here will have access to the correlation ID
52
+ const id = getCorrelationId(); // Returns "$correlationId"
53
+ });
54
+ ```
55
+
56
+ #### Getting or generating a correlation ID
57
+
58
+ ```typescript
59
+ import { getOrGenerateCorrelationId } from "@worknice/instrumentation";
60
+
61
+ const { correlationId, isNew } = getOrGenerateCorrelationId();
62
+ ```
63
+
64
+ #### Extracting from HTTP headers
65
+
66
+ Extract correlation ID from HTTP headers in order:
67
+
68
+ - `x-request-id`
69
+ - `x-correlation-id`
70
+ - `request-id`
71
+ - `correlation-id`
72
+
73
+ ```typescript
74
+ import { extractCorrelationIdFromHeaders } from "@worknice/instrumentation";
75
+
76
+ const correlationId = extractCorrelationIdFromHeaders(request.headers);
77
+ ```
78
+
79
+ ## Configuration
80
+
81
+ ### Environment Variables
82
+
83
+ The package supports the following environment variables:
84
+
85
+ - `AXIOM_DATASET`: Axiom dataset name for error logging
86
+ - `AXIOM_TOKEN`: Axiom API token for authentication
87
+ - `NODE_ENV`: Environment name (development, staging, production)
88
+
89
+ When Axiom credentials are not provided, the package falls back to console logging.
90
+
91
+ ## Architecture
92
+
93
+ ### AsyncLocalStorage
94
+
95
+ The package uses Node.js AsyncLocalStorage to maintain correlation context across async boundaries without explicitly passing IDs through function calls.
96
+ This allows correlation IDs to automatically flow through:
97
+
98
+ - GraphQL operations
99
+ - Database queries
100
+ - API calls
101
+ - Background jobs
102
+ - Nested async operations
103
+
104
+ ## Development
105
+
106
+ ### Building
107
+
108
+ ```bash
109
+ pnpm --filter=@worknice/instrumentation build
110
+ ```
111
+
112
+ ### Development Mode
113
+
114
+ ```bash
115
+ pnpm --filter=@worknice/instrumentation dev
116
+ ```
117
+
118
+ ### Testing & Linting
119
+
120
+ ```bash
121
+ pnpm --filter=@worknice/instrumentation test:lint
122
+ pnpm --filter=@worknice/instrumentation test:types
123
+ pnpm --filter=@worknice/instrumentation test:format
124
+ ```
125
+
126
+ ## Use Cases
127
+
128
+ ### Cross-Service Request Tracing
129
+
130
+ When a request flows through multiple services (e.g., Notebook → MYOB → External API), the correlation ID helps trace the entire request chain:
131
+
132
+ **In Notebook app:**
133
+
134
+ ```typescript
135
+ import { getOrGenerateCorrelationId } from "@worknice/instrumentation";
136
+
137
+ const { correlationId } = getOrGenerateCorrelationId();
138
+ await fetch("https://myob-service/api/endpoint", {
139
+ headers: {
140
+ "x-correlation-id": correlationId,
141
+ },
142
+ });
143
+ ```
144
+
145
+ **In MYOB service:**
146
+
147
+ ```typescript
148
+ import { extractCorrelationIdFromHeaders, runWithCorrelationId } from "@worknice/instrumentation";
149
+
150
+ const correlationId = extractCorrelationIdFromHeaders(req.headers);
151
+ runWithCorrelationId(correlationId, async () => {
152
+ // All operations here share the same correlation ID
153
+ await processRequest();
154
+ });
155
+ ```
156
+
157
+ ### Background Job Tracking
158
+
159
+ Track background jobs initiated from web requests:
160
+
161
+ **In API route:**
162
+
163
+ ```typescript
164
+ import { getCorrelationId } from "@worknice/instrumentation";
165
+
166
+ const correlationId = getCorrelationId();
167
+ await inngest.send({
168
+ name: "process.data",
169
+ data: { correlationId, ...payload },
170
+ });
171
+ ```
172
+
173
+ **In Inngest function:**
174
+
175
+ ```typescript
176
+ import { runWithCorrelationId } from "@worknice/instrumentation";
177
+
178
+ export const processData = inngest.createFunction(
179
+ { id: "process-data" },
180
+ { event: "process.data" },
181
+ async ({ event }) => {
182
+ return runWithCorrelationId(event.data.correlationId, async () => {
183
+ // Job execution with correlation tracking
184
+ });
185
+ }
186
+ );
187
+ ```
188
+
189
+ ## Troubleshooting
190
+
191
+ ### Correlation ID Not Available
192
+
193
+ If `getCorrelationId()` returns undefined:
194
+
195
+ 1. Ensure you're within a correlation context (use `runWithCorrelationId`)
196
+ 2. Verify the context hasn't been lost (e.g., through `setTimeout` without binding)
197
+
198
+ ### Axiom Logs Not Appearing
199
+
200
+ 1. Verify `AXIOM_DATASET` and `AXIOM_TOKEN` environment variables are set
201
+ 2. Check console output on startup for "Axiom error logging enabled" message
202
+ 3. Ensure the Axiom dataset exists and token has write permissions
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Correlation ID management with lazy-loaded Node.js modules
3
+ * Node.js-specific imports are loaded inside functions to support Edge compilation
4
+ */
5
+ /**
6
+ * Correlation context stored in AsyncLocalStorage
7
+ * Provides request-scoped correlation ID management
8
+ */
9
+ interface CorrelationContext {
10
+ correlationId: string;
11
+ }
12
+ /**
13
+ * Initialise the AsyncLocalStorage instance
14
+ * Must be called once at app startup (e.g., in instrumentation.ts)
15
+ * Lazy loads the node:async_hooks module to avoid Edge compilation issues
16
+ */
17
+ declare const initializeCorrelationStorage: () => Promise<void>;
18
+ /**
19
+ * Get the current correlation ID from async context
20
+ * Returns undefined if not in a correlation context or if storage not initialised
21
+ * Note: initializeCorrelationStorage() must be called at app startup
22
+ */
23
+ declare const getCorrelationId: () => string | undefined;
24
+ /**
25
+ * Get the full correlation context
26
+ * Returns undefined if not in a correlation context or if storage not initialised
27
+ * Note: initializeCorrelationStorage() must be called at app startup
28
+ */
29
+ declare const getCorrelationContext: () => CorrelationContext | undefined;
30
+ /**
31
+ * Generate a new correlation ID
32
+ * Ensures consistent ID generation across the application
33
+ * Edge-safe: uses crypto.randomUUID() which works in both runtimes
34
+ *
35
+ * @returns A new UUID v4 correlation ID
36
+ */
37
+ declare const generateCorrelationId: () => string;
38
+ /**
39
+ * Run a function with a specific correlation ID
40
+ * Creates a new async context with the provided correlation ID
41
+ * Ensures storage is initialised before running
42
+ */
43
+ declare function runWithCorrelationId<T>(correlationId: string, fn: () => T | Promise<T>): Promise<T>;
44
+ /**
45
+ * Get or generate a correlation ID
46
+ * Checks async context first, then generates a new UUID if needed
47
+ * Note: This does not set the correlation ID in async context.
48
+ * Use runWithCorrelationId to establish context.
49
+ */
50
+ declare const getOrGenerateCorrelationId: () => {
51
+ correlationId: string;
52
+ isNew: boolean;
53
+ };
54
+ /**
55
+ * Check if we're currently in a correlation context
56
+ */
57
+ declare const hasCorrelationContext: () => boolean;
58
+ /**
59
+ * Extract correlation ID from HTTP headers
60
+ * Supports both Node.js IncomingHttpHeaders and Fetch API Headers
61
+ * Edge-safe: only uses header parsing, no Node.js-specific APIs
62
+ *
63
+ * @param headers - Either Node.js headers object or Fetch API Headers
64
+ * @returns The correlation ID if found, undefined otherwise
65
+ */
66
+ declare const extractCorrelationIdFromHeaders: (headers: Headers | Record<string, string | string[] | undefined>) => string | undefined;
67
+
68
+ export { type CorrelationContext, extractCorrelationIdFromHeaders, generateCorrelationId, getCorrelationContext, getCorrelationId, getOrGenerateCorrelationId, hasCorrelationContext, initializeCorrelationStorage, runWithCorrelationId };
@@ -0,0 +1,96 @@
1
+ let correlationStorage;
2
+ let initializationPromise;
3
+ const initializeCorrelationStorage = async () => {
4
+ if (correlationStorage) {
5
+ return;
6
+ }
7
+ if (initializationPromise) {
8
+ await initializationPromise;
9
+ return;
10
+ }
11
+ initializationPromise = (async () => {
12
+ try {
13
+ const { AsyncLocalStorage } = await import("node:async_hooks");
14
+ correlationStorage = new AsyncLocalStorage();
15
+ return correlationStorage;
16
+ } catch (error) {
17
+ console.warn("Failed to initialize AsyncLocalStorage (expected in Edge runtime):", error);
18
+ return void 0;
19
+ }
20
+ })();
21
+ await initializationPromise;
22
+ };
23
+ const getCorrelationId = () => {
24
+ if (!correlationStorage) {
25
+ return void 0;
26
+ }
27
+ return correlationStorage.getStore()?.correlationId;
28
+ };
29
+ const getCorrelationContext = () => {
30
+ if (!correlationStorage) {
31
+ return void 0;
32
+ }
33
+ return correlationStorage.getStore();
34
+ };
35
+ const generateCorrelationId = () => {
36
+ return crypto.randomUUID();
37
+ };
38
+ async function runWithCorrelationId(correlationId, fn) {
39
+ const context = {
40
+ correlationId
41
+ };
42
+ await initializeCorrelationStorage();
43
+ if (!correlationStorage) {
44
+ console.warn("AsyncLocalStorage not available, executing without context");
45
+ return fn();
46
+ }
47
+ return correlationStorage.run(context, fn);
48
+ }
49
+ const getOrGenerateCorrelationId = () => {
50
+ const existing = getCorrelationId();
51
+ if (existing) {
52
+ return { correlationId: existing, isNew: false };
53
+ }
54
+ return { correlationId: crypto.randomUUID(), isNew: true };
55
+ };
56
+ const hasCorrelationContext = () => {
57
+ return getCorrelationContext() !== void 0;
58
+ };
59
+ const CORRELATION_HEADERS = [
60
+ "x-request-id",
61
+ "x-correlation-id",
62
+ "request-id",
63
+ "correlation-id"
64
+ ];
65
+ const extractCorrelationIdFromHeaders = (headers) => {
66
+ if (headers instanceof Headers) {
67
+ for (const headerName of CORRELATION_HEADERS) {
68
+ const value = headers.get(headerName);
69
+ if (value) return value;
70
+ }
71
+ return void 0;
72
+ }
73
+ for (const headerName of CORRELATION_HEADERS) {
74
+ const value = headers[headerName] || headers[headerName.toLowerCase()];
75
+ if (value) {
76
+ return Array.isArray(value) ? value[0] : value;
77
+ }
78
+ const upperHeaderName = headerName.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("-");
79
+ const upperValue = headers[upperHeaderName];
80
+ if (upperValue) {
81
+ return Array.isArray(upperValue) ? upperValue[0] : upperValue;
82
+ }
83
+ }
84
+ return void 0;
85
+ };
86
+ export {
87
+ extractCorrelationIdFromHeaders,
88
+ generateCorrelationId,
89
+ getCorrelationContext,
90
+ getCorrelationId,
91
+ getOrGenerateCorrelationId,
92
+ hasCorrelationContext,
93
+ initializeCorrelationStorage,
94
+ runWithCorrelationId
95
+ };
96
+ //# sourceMappingURL=correlation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/correlation.ts"],"sourcesContent":["/**\n * Correlation ID management with lazy-loaded Node.js modules\n * Node.js-specific imports are loaded inside functions to support Edge compilation\n */\n\n/**\n * Correlation context stored in AsyncLocalStorage\n * Provides request-scoped correlation ID management\n */\nexport interface CorrelationContext {\n correlationId: string;\n}\n\n/**\n * Minimal type definition for AsyncLocalStorage methods used\n * Avoids module-level imports while maintaining type safety\n */\ninterface AsyncLocalStorageType {\n getStore(): CorrelationContext | undefined;\n run<T>(store: CorrelationContext, fn: () => T | Promise<T>): T | Promise<T>;\n}\n\n/**\n * Lazy-loaded AsyncLocalStorage instance\n * Only initialised when first accessed in Node.js runtime\n */\nlet correlationStorage: AsyncLocalStorageType | undefined;\n\n/**\n * Promise to ensure single initialisation of AsyncLocalStorage\n */\nlet initializationPromise: Promise<AsyncLocalStorageType | undefined> | undefined;\n\n/**\n * Initialise the AsyncLocalStorage instance\n * Must be called once at app startup (e.g., in instrumentation.ts)\n * Lazy loads the node:async_hooks module to avoid Edge compilation issues\n */\nexport const initializeCorrelationStorage = async (): Promise<void> => {\n if (correlationStorage) {\n return;\n }\n\n if (initializationPromise) {\n await initializationPromise;\n return;\n }\n\n initializationPromise = (async () => {\n try {\n const { AsyncLocalStorage } = await import(\"node:async_hooks\");\n correlationStorage = new AsyncLocalStorage<CorrelationContext>();\n return correlationStorage;\n } catch (error) {\n console.warn(\"Failed to initialize AsyncLocalStorage (expected in Edge runtime):\", error);\n return undefined;\n }\n })();\n\n await initializationPromise;\n};\n\n/**\n * Get the current correlation ID from async context\n * Returns undefined if not in a correlation context or if storage not initialised\n * Note: initializeCorrelationStorage() must be called at app startup\n */\nexport const getCorrelationId = (): string | undefined => {\n if (!correlationStorage) {\n return undefined;\n }\n return correlationStorage.getStore()?.correlationId;\n};\n\n/**\n * Get the full correlation context\n * Returns undefined if not in a correlation context or if storage not initialised\n * Note: initializeCorrelationStorage() must be called at app startup\n */\nexport const getCorrelationContext = (): CorrelationContext | undefined => {\n if (!correlationStorage) {\n return undefined;\n }\n return correlationStorage.getStore();\n};\n\n/**\n * Generate a new correlation ID\n * Ensures consistent ID generation across the application\n * Edge-safe: uses crypto.randomUUID() which works in both runtimes\n *\n * @returns A new UUID v4 correlation ID\n */\nexport const generateCorrelationId = (): string => {\n return crypto.randomUUID();\n};\n\n/**\n * Run a function with a specific correlation ID\n * Creates a new async context with the provided correlation ID\n * Ensures storage is initialised before running\n */\nexport async function runWithCorrelationId<T>(\n correlationId: string,\n fn: () => T | Promise<T>,\n): Promise<T> {\n const context: CorrelationContext = {\n correlationId,\n };\n\n await initializeCorrelationStorage();\n\n if (!correlationStorage) {\n console.warn(\"AsyncLocalStorage not available, executing without context\");\n return fn();\n }\n\n return correlationStorage.run(context, fn);\n}\n\n/**\n * Get or generate a correlation ID\n * Checks async context first, then generates a new UUID if needed\n * Note: This does not set the correlation ID in async context.\n * Use runWithCorrelationId to establish context.\n */\nexport const getOrGenerateCorrelationId = (): {\n correlationId: string;\n isNew: boolean;\n} => {\n const existing = getCorrelationId();\n if (existing) {\n return { correlationId: existing, isNew: false };\n }\n\n return { correlationId: crypto.randomUUID(), isNew: true };\n};\n\n/**\n * Check if we're currently in a correlation context\n */\nexport const hasCorrelationContext = (): boolean => {\n return getCorrelationContext() !== undefined;\n};\n\n/**\n * Standard header names for correlation IDs (in order of preference)\n * Note: Fetch API Headers.get() is case-insensitive, but Node.js headers are case-sensitive\n */\nconst CORRELATION_HEADERS = [\n \"x-request-id\",\n \"x-correlation-id\",\n \"request-id\",\n \"correlation-id\",\n] as const;\n\n/**\n * Extract correlation ID from HTTP headers\n * Supports both Node.js IncomingHttpHeaders and Fetch API Headers\n * Edge-safe: only uses header parsing, no Node.js-specific APIs\n *\n * @param headers - Either Node.js headers object or Fetch API Headers\n * @returns The correlation ID if found, undefined otherwise\n */\nexport const extractCorrelationIdFromHeaders = (\n headers: Headers | Record<string, string | string[] | undefined>,\n): string | undefined => {\n if (headers instanceof Headers) {\n for (const headerName of CORRELATION_HEADERS) {\n const value = headers.get(headerName);\n if (value) return value;\n }\n return undefined;\n }\n\n for (const headerName of CORRELATION_HEADERS) {\n const value = headers[headerName] || headers[headerName.toLowerCase()];\n if (value) {\n // Handle array values (Node.js can return arrays for headers)\n return Array.isArray(value) ? value[0] : value;\n }\n\n const upperHeaderName = headerName\n .split(\"-\")\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(\"-\");\n\n const upperValue = headers[upperHeaderName];\n if (upperValue) {\n return Array.isArray(upperValue) ? upperValue[0] : upperValue;\n }\n }\n\n return undefined;\n};\n"],"mappings":"AA0BA,IAAI;AAKJ,IAAI;AAOG,MAAM,+BAA+B,YAA2B;AACrE,MAAI,oBAAoB;AACtB;AAAA,EACF;AAEA,MAAI,uBAAuB;AACzB,UAAM;AACN;AAAA,EACF;AAEA,2BAAyB,YAAY;AACnC,QAAI;AACF,YAAM,EAAE,kBAAkB,IAAI,MAAM,OAAO,kBAAkB;AAC7D,2BAAqB,IAAI,kBAAsC;AAC/D,aAAO;AAAA,IACT,SAAS,OAAO;AACd,cAAQ,KAAK,sEAAsE,KAAK;AACxF,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAEH,QAAM;AACR;AAOO,MAAM,mBAAmB,MAA0B;AACxD,MAAI,CAAC,oBAAoB;AACvB,WAAO;AAAA,EACT;AACA,SAAO,mBAAmB,SAAS,GAAG;AACxC;AAOO,MAAM,wBAAwB,MAAsC;AACzE,MAAI,CAAC,oBAAoB;AACvB,WAAO;AAAA,EACT;AACA,SAAO,mBAAmB,SAAS;AACrC;AASO,MAAM,wBAAwB,MAAc;AACjD,SAAO,OAAO,WAAW;AAC3B;AAOA,eAAsB,qBACpB,eACA,IACY;AACZ,QAAM,UAA8B;AAAA,IAClC;AAAA,EACF;AAEA,QAAM,6BAA6B;AAEnC,MAAI,CAAC,oBAAoB;AACvB,YAAQ,KAAK,4DAA4D;AACzE,WAAO,GAAG;AAAA,EACZ;AAEA,SAAO,mBAAmB,IAAI,SAAS,EAAE;AAC3C;AAQO,MAAM,6BAA6B,MAGrC;AACH,QAAM,WAAW,iBAAiB;AAClC,MAAI,UAAU;AACZ,WAAO,EAAE,eAAe,UAAU,OAAO,MAAM;AAAA,EACjD;AAEA,SAAO,EAAE,eAAe,OAAO,WAAW,GAAG,OAAO,KAAK;AAC3D;AAKO,MAAM,wBAAwB,MAAe;AAClD,SAAO,sBAAsB,MAAM;AACrC;AAMA,MAAM,sBAAsB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAUO,MAAM,kCAAkC,CAC7C,YACuB;AACvB,MAAI,mBAAmB,SAAS;AAC9B,eAAW,cAAc,qBAAqB;AAC5C,YAAM,QAAQ,QAAQ,IAAI,UAAU;AACpC,UAAI,MAAO,QAAO;AAAA,IACpB;AACA,WAAO;AAAA,EACT;AAEA,aAAW,cAAc,qBAAqB;AAC5C,UAAM,QAAQ,QAAQ,UAAU,KAAK,QAAQ,WAAW,YAAY,CAAC;AACrE,QAAI,OAAO;AAET,aAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,CAAC,IAAI;AAAA,IAC3C;AAEA,UAAM,kBAAkB,WACrB,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,GAAG;AAEX,UAAM,aAAa,QAAQ,eAAe;AAC1C,QAAI,YAAY;AACd,aAAO,MAAM,QAAQ,UAAU,IAAI,WAAW,CAAC,IAAI;AAAA,IACrD;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
@@ -0,0 +1,4 @@
1
+ export { extractCorrelationIdFromHeaders, generateCorrelationId, getCorrelationContext, getCorrelationId, getOrGenerateCorrelationId, hasCorrelationContext, runWithCorrelationId } from './correlation.js';
2
+ export { onRequestError, register } from './instrumentation.js';
3
+ export { default as withCorrelationId } from './serverAction.js';
4
+ import 'next';
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ import {
2
+ extractCorrelationIdFromHeaders,
3
+ generateCorrelationId,
4
+ getCorrelationContext,
5
+ getCorrelationId,
6
+ getOrGenerateCorrelationId,
7
+ hasCorrelationContext,
8
+ runWithCorrelationId
9
+ } from "./correlation.js";
10
+ import { onRequestError, register } from "./instrumentation.js";
11
+ import { withCorrelationId } from "./serverAction.js";
12
+ export {
13
+ extractCorrelationIdFromHeaders,
14
+ generateCorrelationId,
15
+ getCorrelationContext,
16
+ getCorrelationId,
17
+ getOrGenerateCorrelationId,
18
+ hasCorrelationContext,
19
+ onRequestError,
20
+ register,
21
+ runWithCorrelationId,
22
+ withCorrelationId
23
+ };
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @worknice/instrumentation\n *\n * Shared instrumentation and correlation ID management for Worknice applications\n */\n\nexport {\n extractCorrelationIdFromHeaders,\n generateCorrelationId,\n getCorrelationContext,\n getCorrelationId,\n getOrGenerateCorrelationId,\n hasCorrelationContext,\n runWithCorrelationId,\n} from \"./correlation.js\";\nexport { onRequestError, register } from \"./instrumentation.js\";\nexport { withCorrelationId } from \"./serverAction.js\";\n"],"mappings":"AAMA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB,gBAAgB;AACzC,SAAS,yBAAyB;","names":[]}
@@ -0,0 +1,12 @@
1
+ import { Instrumentation } from 'next';
2
+
3
+ /**
4
+ * Node.js-specific instrumentation
5
+ * This file is only loaded when running in Node.js runtime
6
+ * It has full access to Node.js APIs
7
+ */
8
+
9
+ declare function register(): Promise<void>;
10
+ declare const onRequestError: Instrumentation.onRequestError;
11
+
12
+ export { onRequestError, register };
@@ -0,0 +1,77 @@
1
+ import { WinstonTransport as AxiomTransport } from "@axiomhq/winston";
2
+ import winston from "winston";
3
+ import {
4
+ extractCorrelationIdFromHeaders,
5
+ getCorrelationId,
6
+ initializeCorrelationStorage
7
+ } from "./correlation.js";
8
+ const dynamicMetaFormat = winston.format((info) => {
9
+ const correlationId = getCorrelationId();
10
+ return correlationId ? { ...info, correlationId } : info;
11
+ });
12
+ const createErrorLogger = () => {
13
+ const axiomDataset = process.env.AXIOM_DATASET;
14
+ const axiomToken = process.env.AXIOM_TOKEN;
15
+ return winston.createLogger({
16
+ level: "error",
17
+ format: winston.format.combine(dynamicMetaFormat(), winston.format.json()),
18
+ defaultMeta: {
19
+ source: "instrumentation",
20
+ environment: process.env.NODE_ENV || "development",
21
+ runtime: "nodejs"
22
+ },
23
+ transports: [
24
+ axiomDataset && axiomToken ? new AxiomTransport({
25
+ dataset: axiomDataset,
26
+ token: axiomToken
27
+ }) : new winston.transports.Console()
28
+ ]
29
+ });
30
+ };
31
+ const errorLogger = createErrorLogger();
32
+ async function register() {
33
+ await initializeCorrelationStorage();
34
+ console.log("\n\n========================================");
35
+ console.log("[Instrumentation] Correlation tracking initialised (Node.js runtime)");
36
+ if (process.env.AXIOM_DATASET && process.env.AXIOM_TOKEN) {
37
+ console.log("[Instrumentation] Axiom error logging enabled");
38
+ } else {
39
+ console.log("[Instrumentation] Axiom not configured - using console for errors");
40
+ }
41
+ console.log("========================================\n\n");
42
+ }
43
+ const onRequestError = (error, request, context) => {
44
+ const typedError = error;
45
+ let correlationId = getCorrelationId();
46
+ if (!correlationId) {
47
+ correlationId = extractCorrelationIdFromHeaders(request.headers);
48
+ }
49
+ const errorData = {
50
+ correlationId,
51
+ digest: typedError.digest,
52
+ error: {
53
+ message: typedError.message,
54
+ stack: typedError.stack,
55
+ name: typedError.name
56
+ },
57
+ request: {
58
+ path: request.path,
59
+ method: request.method,
60
+ headers: request.headers
61
+ },
62
+ context: {
63
+ routerKind: context.routerKind,
64
+ routePath: context.routePath,
65
+ routeType: context.routeType,
66
+ renderSource: context.renderSource,
67
+ revalidateReason: context.revalidateReason
68
+ },
69
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
70
+ };
71
+ errorLogger.error("[Request Error]", errorData);
72
+ };
73
+ export {
74
+ onRequestError,
75
+ register
76
+ };
77
+ //# sourceMappingURL=instrumentation-node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/instrumentation-node.ts"],"sourcesContent":["/**\n * Node.js-specific instrumentation\n * This file is only loaded when running in Node.js runtime\n * It has full access to Node.js APIs\n */\n\nimport { WinstonTransport as AxiomTransport } from \"@axiomhq/winston\";\nimport type { Instrumentation } from \"next\";\nimport winston from \"winston\";\n\nimport {\n extractCorrelationIdFromHeaders,\n getCorrelationId,\n initializeCorrelationStorage,\n} from \"./correlation.js\";\n\nconst dynamicMetaFormat = winston.format((info) => {\n const correlationId = getCorrelationId();\n return correlationId ? { ...info, correlationId } : info;\n});\n\n/**\n * Create Axiom logger for error reporting\n * Falls back to console if Axiom credentials are not configured\n */\nconst createErrorLogger = () => {\n const axiomDataset = process.env.AXIOM_DATASET;\n const axiomToken = process.env.AXIOM_TOKEN;\n\n return winston.createLogger({\n level: \"error\",\n format: winston.format.combine(dynamicMetaFormat(), winston.format.json()),\n defaultMeta: {\n source: \"instrumentation\",\n environment: process.env.NODE_ENV || \"development\",\n runtime: \"nodejs\",\n },\n transports: [\n axiomDataset && axiomToken\n ? new AxiomTransport({\n dataset: axiomDataset,\n token: axiomToken,\n })\n : new winston.transports.Console(),\n ],\n });\n};\n\nconst errorLogger = createErrorLogger();\n\nexport async function register() {\n await initializeCorrelationStorage();\n\n console.log(\"\\n\\n========================================\");\n console.log(\"[Instrumentation] Correlation tracking initialised (Node.js runtime)\");\n if (process.env.AXIOM_DATASET && process.env.AXIOM_TOKEN) {\n console.log(\"[Instrumentation] Axiom error logging enabled\");\n } else {\n console.log(\"[Instrumentation] Axiom not configured - using console for errors\");\n }\n console.log(\"========================================\\n\\n\");\n}\n\nexport const onRequestError: Instrumentation.onRequestError = (error, request, context) => {\n const typedError = error as Error & { digest?: string };\n\n let correlationId = getCorrelationId();\n if (!correlationId) {\n correlationId = extractCorrelationIdFromHeaders(request.headers);\n }\n\n const errorData = {\n correlationId,\n digest: typedError.digest,\n error: {\n message: typedError.message,\n stack: typedError.stack,\n name: typedError.name,\n },\n request: {\n path: request.path,\n method: request.method,\n headers: request.headers,\n },\n context: {\n routerKind: context.routerKind,\n routePath: context.routePath,\n routeType: context.routeType,\n renderSource: context.renderSource,\n revalidateReason: context.revalidateReason,\n },\n timestamp: new Date().toISOString(),\n };\n\n errorLogger.error(\"[Request Error]\", errorData);\n};\n"],"mappings":"AAMA,SAAS,oBAAoB,sBAAsB;AAEnD,OAAO,aAAa;AAEpB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,oBAAoB,QAAQ,OAAO,CAAC,SAAS;AACjD,QAAM,gBAAgB,iBAAiB;AACvC,SAAO,gBAAgB,EAAE,GAAG,MAAM,cAAc,IAAI;AACtD,CAAC;AAMD,MAAM,oBAAoB,MAAM;AAC9B,QAAM,eAAe,QAAQ,IAAI;AACjC,QAAM,aAAa,QAAQ,IAAI;AAE/B,SAAO,QAAQ,aAAa;AAAA,IAC1B,OAAO;AAAA,IACP,QAAQ,QAAQ,OAAO,QAAQ,kBAAkB,GAAG,QAAQ,OAAO,KAAK,CAAC;AAAA,IACzE,aAAa;AAAA,MACX,QAAQ;AAAA,MACR,aAAa,QAAQ,IAAI,YAAY;AAAA,MACrC,SAAS;AAAA,IACX;AAAA,IACA,YAAY;AAAA,MACV,gBAAgB,aACZ,IAAI,eAAe;AAAA,QACjB,SAAS;AAAA,QACT,OAAO;AAAA,MACT,CAAC,IACD,IAAI,QAAQ,WAAW,QAAQ;AAAA,IACrC;AAAA,EACF,CAAC;AACH;AAEA,MAAM,cAAc,kBAAkB;AAEtC,eAAsB,WAAW;AAC/B,QAAM,6BAA6B;AAEnC,UAAQ,IAAI,8CAA8C;AAC1D,UAAQ,IAAI,sEAAsE;AAClF,MAAI,QAAQ,IAAI,iBAAiB,QAAQ,IAAI,aAAa;AACxD,YAAQ,IAAI,+CAA+C;AAAA,EAC7D,OAAO;AACL,YAAQ,IAAI,mEAAmE;AAAA,EACjF;AACA,UAAQ,IAAI,8CAA8C;AAC5D;AAEO,MAAM,iBAAiD,CAAC,OAAO,SAAS,YAAY;AACzF,QAAM,aAAa;AAEnB,MAAI,gBAAgB,iBAAiB;AACrC,MAAI,CAAC,eAAe;AAClB,oBAAgB,gCAAgC,QAAQ,OAAO;AAAA,EACjE;AAEA,QAAM,YAAY;AAAA,IAChB;AAAA,IACA,QAAQ,WAAW;AAAA,IACnB,OAAO;AAAA,MACL,SAAS,WAAW;AAAA,MACpB,OAAO,WAAW;AAAA,MAClB,MAAM,WAAW;AAAA,IACnB;AAAA,IACA,SAAS;AAAA,MACP,MAAM,QAAQ;AAAA,MACd,QAAQ,QAAQ;AAAA,MAChB,SAAS,QAAQ;AAAA,IACnB;AAAA,IACA,SAAS;AAAA,MACP,YAAY,QAAQ;AAAA,MACpB,WAAW,QAAQ;AAAA,MACnB,WAAW,QAAQ;AAAA,MACnB,cAAc,QAAQ;AAAA,MACtB,kBAAkB,QAAQ;AAAA,IAC5B;AAAA,IACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,EACpC;AAEA,cAAY,MAAM,mBAAmB,SAAS;AAChD;","names":[]}
@@ -0,0 +1,40 @@
1
+ import { Instrumentation } from 'next';
2
+
3
+ /**
4
+ * Next.js Instrumentation Entry Point
5
+ *
6
+ * This file serves as an Edge-safe entry point for instrumentation across all Next.js apps
7
+ * in the monorepo. It uses dynamic imports to prevent Node.js-specific modules from breaking
8
+ * Edge runtime compilation, even though our apps use Node.js runtime exclusively.
9
+ *
10
+ * Architecture:
11
+ * - instrumentation.ts (this file): Edge-safe entry point with dynamic imports
12
+ * - instrumentation-node.ts: Node.js implementation with Winston/Axiom logging
13
+ *
14
+ * The separation is necessary because Next.js compiles instrumentation for both runtimes
15
+ * regardless of the runtime configuration, and Winston/Axiom imports would cause Edge
16
+ * compilation errors if placed directly in this file.
17
+ *
18
+ * See https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation for more details.
19
+ */
20
+
21
+ /**
22
+ * Initialize instrumentation for the application
23
+ *
24
+ * Dynamically loads the Node.js implementation which:
25
+ * - Initializes AsyncLocalStorage for correlation tracking
26
+ * - Sets up Winston logging with Axiom transport
27
+ * - Configures error reporting with correlation context
28
+ */
29
+ declare function register(): Promise<void>;
30
+ /**
31
+ * Handle request errors with correlation context
32
+ *
33
+ * Dynamically loads the Node.js error handler which:
34
+ * - Extracts or generates correlation ID
35
+ * - Logs errors to Axiom with full context
36
+ * - Maintains correlation ID for Winston logging
37
+ */
38
+ declare const onRequestError: Instrumentation.onRequestError;
39
+
40
+ export { onRequestError, register };
@@ -0,0 +1,13 @@
1
+ async function register() {
2
+ const { register: registerNode } = await import("./instrumentation-node.js");
3
+ return registerNode();
4
+ }
5
+ const onRequestError = async (error, request, context) => {
6
+ const { onRequestError: nodeHandler } = await import("./instrumentation-node.js");
7
+ return nodeHandler(error, request, context);
8
+ };
9
+ export {
10
+ onRequestError,
11
+ register
12
+ };
13
+ //# sourceMappingURL=instrumentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/instrumentation.ts"],"sourcesContent":["/**\n * Next.js Instrumentation Entry Point\n *\n * This file serves as an Edge-safe entry point for instrumentation across all Next.js apps\n * in the monorepo. It uses dynamic imports to prevent Node.js-specific modules from breaking\n * Edge runtime compilation, even though our apps use Node.js runtime exclusively.\n *\n * Architecture:\n * - instrumentation.ts (this file): Edge-safe entry point with dynamic imports\n * - instrumentation-node.ts: Node.js implementation with Winston/Axiom logging\n *\n * The separation is necessary because Next.js compiles instrumentation for both runtimes\n * regardless of the runtime configuration, and Winston/Axiom imports would cause Edge\n * compilation errors if placed directly in this file.\n *\n * See https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation for more details.\n */\n\nimport type { Instrumentation } from \"next\";\n\n/**\n * Initialize instrumentation for the application\n *\n * Dynamically loads the Node.js implementation which:\n * - Initializes AsyncLocalStorage for correlation tracking\n * - Sets up Winston logging with Axiom transport\n * - Configures error reporting with correlation context\n */\nexport async function register() {\n const { register: registerNode } = await import(\"./instrumentation-node.js\");\n return registerNode();\n}\n\n/**\n * Handle request errors with correlation context\n *\n * Dynamically loads the Node.js error handler which:\n * - Extracts or generates correlation ID\n * - Logs errors to Axiom with full context\n * - Maintains correlation ID for Winston logging\n */\nexport const onRequestError: Instrumentation.onRequestError = async (error, request, context) => {\n const { onRequestError: nodeHandler } = await import(\"./instrumentation-node.js\");\n return nodeHandler(error, request, context);\n};\n"],"mappings":"AA4BA,eAAsB,WAAW;AAC/B,QAAM,EAAE,UAAU,aAAa,IAAI,MAAM,OAAO,2BAA2B;AAC3E,SAAO,aAAa;AACtB;AAUO,MAAM,iBAAiD,OAAO,OAAO,SAAS,YAAY;AAC/F,QAAM,EAAE,gBAAgB,YAAY,IAAI,MAAM,OAAO,2BAA2B;AAChF,SAAO,YAAY,OAAO,SAAS,OAAO;AAC5C;","names":[]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Higher-order function for Next.js server actions with automatic correlation ID handling.
3
+ *
4
+ * This wrapper automatically establishes correlation context for server actions,
5
+ * eliminating the need for boilerplate correlation ID code in each action.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Using withCorrelationId
10
+ * const myAction = async (data: Data) => {
11
+ * // ... action logic (correlation context automatically available via getCorrelationId())
12
+ * };
13
+ * export default withCorrelationId(myAction);
14
+ * ```
15
+ */
16
+ /**
17
+ * Wraps a server action function with automatic correlation ID context.
18
+ *
19
+ * The returned function:
20
+ * 1. Gets or generates a correlation ID
21
+ * 2. Establishes correlation context using runWithCorrelationId
22
+ * 3. Executes the original handler within that context
23
+ * 4. Returns the handler's result
24
+ *
25
+ * @param handler - The server action function to wrap
26
+ * @returns A wrapped version with automatic correlation context
27
+ */
28
+ declare function withCorrelationId<T extends any[], R>(handler: (...args: T) => Promise<R>): (...args: T) => Promise<R>;
29
+
30
+ export { withCorrelationId as default, withCorrelationId };
@@ -0,0 +1,13 @@
1
+ import { getOrGenerateCorrelationId, runWithCorrelationId } from "./correlation.js";
2
+ function withCorrelationId(handler) {
3
+ return async (...args) => {
4
+ const { correlationId } = getOrGenerateCorrelationId();
5
+ return runWithCorrelationId(correlationId, () => handler(...args));
6
+ };
7
+ }
8
+ var serverAction_default = withCorrelationId;
9
+ export {
10
+ serverAction_default as default,
11
+ withCorrelationId
12
+ };
13
+ //# sourceMappingURL=serverAction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/serverAction.ts"],"sourcesContent":["/**\n * Higher-order function for Next.js server actions with automatic correlation ID handling.\n *\n * This wrapper automatically establishes correlation context for server actions,\n * eliminating the need for boilerplate correlation ID code in each action.\n *\n * @example\n * ```typescript\n * // Using withCorrelationId\n * const myAction = async (data: Data) => {\n * // ... action logic (correlation context automatically available via getCorrelationId())\n * };\n * export default withCorrelationId(myAction);\n * ```\n */\n\nimport { getOrGenerateCorrelationId, runWithCorrelationId } from \"./correlation.js\";\n\n/**\n * Wraps a server action function with automatic correlation ID context.\n *\n * The returned function:\n * 1. Gets or generates a correlation ID\n * 2. Establishes correlation context using runWithCorrelationId\n * 3. Executes the original handler within that context\n * 4. Returns the handler's result\n *\n * @param handler - The server action function to wrap\n * @returns A wrapped version with automatic correlation context\n */\nexport function withCorrelationId<T extends any[], R>(\n handler: (...args: T) => Promise<R>,\n): (...args: T) => Promise<R> {\n return async (...args: T): Promise<R> => {\n const { correlationId } = getOrGenerateCorrelationId();\n return runWithCorrelationId(correlationId, () => handler(...args));\n };\n}\n\nexport default withCorrelationId;\n"],"mappings":"AAgBA,SAAS,4BAA4B,4BAA4B;AAc1D,SAAS,kBACd,SAC4B;AAC5B,SAAO,UAAU,SAAwB;AACvC,UAAM,EAAE,cAAc,IAAI,2BAA2B;AACrD,WAAO,qBAAqB,eAAe,MAAM,QAAQ,GAAG,IAAI,CAAC;AAAA,EACnE;AACF;AAEA,IAAO,uBAAQ;","names":[]}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@worknice/instrumentation",
3
+ "version": "1.0.1",
4
+ "license": "MIT",
5
+ "private": false,
6
+ "description": "Shared instrumentation management for Worknice applications",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ },
16
+ "./*": {
17
+ "import": "./dist/*.js",
18
+ "types": "./dist/*.d.ts"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "LICENSE"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "dependencies": {
29
+ "@axiomhq/winston": "^1.2.0",
30
+ "next": "~15.5.0",
31
+ "winston": "^3.15.0"
32
+ },
33
+ "devDependencies": {
34
+ "@anolilab/semantic-release-pnpm": "^1.1.10",
35
+ "@total-typescript/tsconfig": "^1.0.4",
36
+ "@types/node": "^20.10.5",
37
+ "@typescript-eslint/eslint-plugin": "^8.7.0",
38
+ "@typescript-eslint/parser": "^8.7.0",
39
+ "eslint": "^8.56.0",
40
+ "prettier": "^3.1.1",
41
+ "semantic-release": "^24.2.2",
42
+ "semantic-release-scope-filter": "^1.0.0",
43
+ "tsup": "^8.0.1",
44
+ "typescript": "^5.3.3",
45
+ "vitest": "^3.1.1"
46
+ },
47
+ "scripts": {
48
+ "build": "rm -rf ./dist && tsup",
49
+ "dev": "rm -rf ./dist && tsup --watch",
50
+ "fix": "exit 0",
51
+ "fix:lint": "eslint \"src/**/*.{js,ts,tsx}\" --fix",
52
+ "fix:format": "prettier '**/*.{js,ts,tsx,css}' --write",
53
+ "test": "exit 0",
54
+ "test:unit": "vitest run",
55
+ "test:format": "prettier '**/*.{js,ts,tsx,css}' --check",
56
+ "test:lint": "eslint \"src/**/*.{js,ts,tsx}\"",
57
+ "test:types": "tsc --noEmit --pretty"
58
+ }
59
+ }