@nexttrace/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,78 @@
1
+ # @nexttrace/next
2
+
3
+ > Next.js integration for **NextTrace** — the local-first, privacy-respecting API debugging overlay and developer observability toolkit.
4
+
5
+ ## Features
6
+
7
+ - ⚡ **Zero External Telemetry**: All events remain in local browser/server memory.
8
+ - 🔒 **Deep Secret Redaction**: Automatically redacts Bearer tokens, API keys, passwords, and custom sensitive fields.
9
+ - 🎯 **App Router Native**: Seamlessly captures client-side `fetch` and outgoing server-side `fetch` via Next.js instrumentation.
10
+ - 🛠️ **Dev-Only Gating**: Tree-shaken in production; renders `null` unless `NODE_ENV === 'development'`.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ # npm
16
+ npm install --save-dev @nexttrace/next @nexttrace/core
17
+
18
+ # pnpm
19
+ pnpm add -D @nexttrace/next @nexttrace/core
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ### 1. Mount the Client Overlay
25
+
26
+ Add `<NextTraceOverlay />` to your root layout (`app/layout.tsx`):
27
+
28
+ ```tsx
29
+ import { NextTraceOverlay } from '@nexttrace/next';
30
+
31
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
32
+ return (
33
+ <html lang="en">
34
+ <body>
35
+ {children}
36
+ <NextTraceOverlay defaultPosition="bottom" />
37
+ </body>
38
+ </html>
39
+ );
40
+ }
41
+ ```
42
+
43
+ ### 2. (Optional) Trace Server-Side `fetch`
44
+
45
+ To capture outgoing `fetch` calls made inside Server Components, Server Actions, or Route Handlers:
46
+
47
+ 1. Enable the instrumentation hook in `next.config.js`:
48
+ ```js
49
+ /** @type {import('next').NextConfig} */
50
+ const nextConfig = {
51
+ experimental: {
52
+ instrumentationHook: true,
53
+ },
54
+ };
55
+
56
+ module.exports = nextConfig;
57
+ ```
58
+
59
+ 2. Register server tracing in `instrumentation.ts` in your project root:
60
+ ```ts
61
+ export async function register() {
62
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
63
+ const { traceNextServer } = await import('@nexttrace/next/server');
64
+ traceNextServer();
65
+ }
66
+ }
67
+ ```
68
+
69
+ 3. Expose the dev-only collector route at `app/api/__nexttrace/route.ts`:
70
+ ```ts
71
+ import { createNextTraceRouteHandler } from '@nexttrace/next/server';
72
+
73
+ export const { GET, POST } = createNextTraceRouteHandler();
74
+ ```
75
+
76
+ ## License
77
+
78
+ MIT © NextTrace Team
@@ -0,0 +1,191 @@
1
+ // src/server.ts
2
+ import {
3
+ redactHeaders,
4
+ sanitizeUrlAndQuery,
5
+ sanitizeBodyPreview,
6
+ DEFAULT_DENY_HEADERS,
7
+ DEFAULT_DENY_QUERY_PARAMS,
8
+ DEFAULT_DENY_JSON_KEYS
9
+ } from "@nexttrace/core";
10
+ var serverEventQueue = [];
11
+ var MAX_SERVER_EVENTS = 100;
12
+ var isServerInstrumented = false;
13
+ var originalGlobalFetch = null;
14
+ function generateEventId() {
15
+ return "srv_" + Math.random().toString(36).substring(2, 10) + Date.now().toString(36);
16
+ }
17
+ function instrumentServerFetch() {
18
+ if (process.env.NODE_ENV !== "development") {
19
+ return () => {
20
+ };
21
+ }
22
+ if (isServerInstrumented || typeof globalThis.fetch === "undefined") {
23
+ return () => {
24
+ };
25
+ }
26
+ originalGlobalFetch = globalThis.fetch;
27
+ isServerInstrumented = true;
28
+ globalThis.fetch = async function(input, init) {
29
+ if (!originalGlobalFetch) return fetch(input, init);
30
+ const urlString = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
31
+ if (urlString.includes("/__nexttrace") || urlString.includes("/_next/")) {
32
+ return originalGlobalFetch.call(globalThis, input, init);
33
+ }
34
+ const eventId = generateEventId();
35
+ const startTime = performance.now();
36
+ const timestamp = Date.now();
37
+ let method = "GET";
38
+ const rawHeaders = {};
39
+ let reqBodyText = null;
40
+ if (init) {
41
+ if (init.method) method = init.method.toUpperCase();
42
+ if (init.headers) {
43
+ if (init.headers instanceof Headers) {
44
+ init.headers.forEach((v, k) => {
45
+ rawHeaders[k] = v;
46
+ });
47
+ } else if (Array.isArray(init.headers)) {
48
+ init.headers.forEach(([k, v]) => {
49
+ rawHeaders[k] = v;
50
+ });
51
+ } else {
52
+ Object.entries(init.headers).forEach(([k, v]) => {
53
+ rawHeaders[k] = String(v);
54
+ });
55
+ }
56
+ }
57
+ if (init.body && typeof init.body === "string") {
58
+ reqBodyText = init.body;
59
+ }
60
+ }
61
+ const { sanitizedUrl, queryParams, wasRedacted: queryRedacted } = sanitizeUrlAndQuery(
62
+ urlString,
63
+ DEFAULT_DENY_QUERY_PARAMS
64
+ );
65
+ const { sanitized: sanitizedReqHeaders, wasRedacted: headersRedacted } = redactHeaders(
66
+ rawHeaders,
67
+ DEFAULT_DENY_HEADERS
68
+ );
69
+ const { preview: reqBodyPreview, wasRedacted: bodyRedacted } = sanitizeBodyPreview(
70
+ reqBodyText,
71
+ 25e3,
72
+ DEFAULT_DENY_JSON_KEYS
73
+ );
74
+ const event = {
75
+ schemaVersion: "1.0.0",
76
+ eventId,
77
+ timestamp,
78
+ phase: "start",
79
+ runtime: "node",
80
+ sourceAdapter: "server-fetch",
81
+ request: {
82
+ method,
83
+ url: urlString,
84
+ sanitizedUrl,
85
+ queryParams,
86
+ headers: sanitizedReqHeaders,
87
+ bodyPreview: reqBodyPreview,
88
+ bodySize: reqBodyText ? reqBodyText.length : 0,
89
+ isTruncated: false
90
+ },
91
+ timing: {
92
+ startTime
93
+ },
94
+ context: {
95
+ attributionMethod: "stack-derived",
96
+ confidence: "medium"
97
+ },
98
+ flags: {
99
+ isInternal: false,
100
+ isRedacted: queryRedacted || headersRedacted || bodyRedacted,
101
+ isTruncated: false,
102
+ isInferred: false
103
+ }
104
+ };
105
+ serverEventQueue.push(event);
106
+ if (serverEventQueue.length > MAX_SERVER_EVENTS) {
107
+ serverEventQueue.shift();
108
+ }
109
+ try {
110
+ const response = await originalGlobalFetch.call(globalThis, input, init);
111
+ const endTime = performance.now();
112
+ const duration = Math.round(endTime - startTime);
113
+ const resHeaders = {};
114
+ response.headers.forEach((v, k) => {
115
+ resHeaders[k] = v;
116
+ });
117
+ const { sanitized: sanitizedResHeaders, wasRedacted: resHeadersRedacted } = redactHeaders(
118
+ resHeaders,
119
+ DEFAULT_DENY_HEADERS
120
+ );
121
+ const contentType = response.headers.get("content-type") || "";
122
+ let resBodyPreview = null;
123
+ if (contentType.includes("application/json") || contentType.includes("text/")) {
124
+ try {
125
+ const clone = response.clone();
126
+ const raw = await clone.text();
127
+ const sanitized = sanitizeBodyPreview(raw, 25e3, DEFAULT_DENY_JSON_KEYS);
128
+ resBodyPreview = sanitized.preview;
129
+ } catch {
130
+ }
131
+ }
132
+ event.phase = response.ok ? "complete" : "error";
133
+ event.timing.endTime = endTime;
134
+ event.timing.duration = duration;
135
+ event.flags.isSlow = duration >= 1e3;
136
+ event.flags.isRedacted = event.flags.isRedacted || resHeadersRedacted;
137
+ event.response = {
138
+ status: response.status,
139
+ statusText: response.statusText,
140
+ headers: sanitizedResHeaders,
141
+ bodyPreview: resBodyPreview,
142
+ contentType,
143
+ bodySize: resBodyPreview ? resBodyPreview.length : 0,
144
+ isTruncated: false
145
+ };
146
+ return response;
147
+ } catch (err) {
148
+ const endTime = performance.now();
149
+ event.phase = "error";
150
+ event.timing.endTime = endTime;
151
+ event.timing.duration = Math.round(endTime - startTime);
152
+ event.error = {
153
+ name: err instanceof Error ? err.name : "ServerError",
154
+ message: err instanceof Error ? err.message : String(err)
155
+ };
156
+ throw err;
157
+ }
158
+ };
159
+ return () => {
160
+ if (isServerInstrumented && originalGlobalFetch) {
161
+ globalThis.fetch = originalGlobalFetch;
162
+ originalGlobalFetch = null;
163
+ isServerInstrumented = false;
164
+ }
165
+ };
166
+ }
167
+ function createNextTraceRouteHandler() {
168
+ return async function GET() {
169
+ if (process.env.NODE_ENV !== "development") {
170
+ return new Response(JSON.stringify({ error: "Forbidden in production" }), {
171
+ status: 403,
172
+ headers: { "Content-Type": "application/json" }
173
+ });
174
+ }
175
+ const events = [...serverEventQueue];
176
+ serverEventQueue.length = 0;
177
+ return new Response(JSON.stringify({ events }), {
178
+ status: 200,
179
+ headers: {
180
+ "Content-Type": "application/json",
181
+ "Cache-Control": "no-store, no-cache",
182
+ "x-nexttrace-internal": "true"
183
+ }
184
+ });
185
+ };
186
+ }
187
+
188
+ export {
189
+ instrumentServerFetch,
190
+ createNextTraceRouteHandler
191
+ };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { NextTraceStore } from '@nexttrace/core';
3
+ import { DockPosition, DevToolsTheme } from '@nexttrace/devtools';
4
+
5
+ interface NextTraceOverlayProps {
6
+ store?: NextTraceStore;
7
+ defaultPosition?: DockPosition;
8
+ defaultTheme?: DevToolsTheme;
9
+ initialOpen?: boolean;
10
+ enableServerSync?: boolean;
11
+ }
12
+ declare const NextTraceOverlay: React.FC<NextTraceOverlayProps>;
13
+
14
+ export { NextTraceOverlay, type NextTraceOverlayProps };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { NextTraceStore } from '@nexttrace/core';
3
+ import { DockPosition, DevToolsTheme } from '@nexttrace/devtools';
4
+
5
+ interface NextTraceOverlayProps {
6
+ store?: NextTraceStore;
7
+ defaultPosition?: DockPosition;
8
+ defaultTheme?: DevToolsTheme;
9
+ initialOpen?: boolean;
10
+ enableServerSync?: boolean;
11
+ }
12
+ declare const NextTraceOverlay: React.FC<NextTraceOverlayProps>;
13
+
14
+ export { NextTraceOverlay, type NextTraceOverlayProps };
package/dist/index.js ADDED
@@ -0,0 +1,83 @@
1
+ 'use client';
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ NextTraceOverlay: () => NextTraceOverlay
25
+ });
26
+ module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/client.tsx
29
+ var import_react = require("react");
30
+ var import_core = require("@nexttrace/core");
31
+ var import_devtools = require("@nexttrace/devtools");
32
+ var import_jsx_runtime = require("react/jsx-runtime");
33
+ var NextTraceOverlay = ({
34
+ store: propStore,
35
+ defaultPosition = "bottom",
36
+ defaultTheme = "dark",
37
+ initialOpen = false,
38
+ enableServerSync = true
39
+ }) => {
40
+ if (process.env.NODE_ENV !== "development") {
41
+ return null;
42
+ }
43
+ const store = propStore || (0, import_core.getGlobalStore)();
44
+ (0, import_react.useEffect)(() => {
45
+ const uninstallFetch = (0, import_core.installFetchInterceptor)(store);
46
+ let syncInterval = null;
47
+ if (enableServerSync) {
48
+ syncInterval = setInterval(async () => {
49
+ try {
50
+ const res = await fetch("/api/__nexttrace", {
51
+ headers: { "x-nexttrace-internal": "true" }
52
+ });
53
+ if (res.ok) {
54
+ const data = await res.json();
55
+ if (data.events && Array.isArray(data.events)) {
56
+ data.events.forEach((srvEvent) => {
57
+ store.recordEvent(srvEvent);
58
+ });
59
+ }
60
+ }
61
+ } catch {
62
+ }
63
+ }, 2e3);
64
+ }
65
+ return () => {
66
+ uninstallFetch();
67
+ if (syncInterval) clearInterval(syncInterval);
68
+ };
69
+ }, [store, enableServerSync]);
70
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
71
+ import_devtools.NextTraceDevTools,
72
+ {
73
+ store,
74
+ defaultPosition,
75
+ defaultTheme,
76
+ initialOpen
77
+ }
78
+ );
79
+ };
80
+ // Annotate the CommonJS export names for ESM import in node:
81
+ 0 && (module.exports = {
82
+ NextTraceOverlay
83
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+
3
+ // src/client.tsx
4
+ import { useEffect } from "react";
5
+ import {
6
+ installFetchInterceptor,
7
+ getGlobalStore
8
+ } from "@nexttrace/core";
9
+ import { NextTraceDevTools } from "@nexttrace/devtools";
10
+ import { jsx } from "react/jsx-runtime";
11
+ var NextTraceOverlay = ({
12
+ store: propStore,
13
+ defaultPosition = "bottom",
14
+ defaultTheme = "dark",
15
+ initialOpen = false,
16
+ enableServerSync = true
17
+ }) => {
18
+ if (process.env.NODE_ENV !== "development") {
19
+ return null;
20
+ }
21
+ const store = propStore || getGlobalStore();
22
+ useEffect(() => {
23
+ const uninstallFetch = installFetchInterceptor(store);
24
+ let syncInterval = null;
25
+ if (enableServerSync) {
26
+ syncInterval = setInterval(async () => {
27
+ try {
28
+ const res = await fetch("/api/__nexttrace", {
29
+ headers: { "x-nexttrace-internal": "true" }
30
+ });
31
+ if (res.ok) {
32
+ const data = await res.json();
33
+ if (data.events && Array.isArray(data.events)) {
34
+ data.events.forEach((srvEvent) => {
35
+ store.recordEvent(srvEvent);
36
+ });
37
+ }
38
+ }
39
+ } catch {
40
+ }
41
+ }, 2e3);
42
+ }
43
+ return () => {
44
+ uninstallFetch();
45
+ if (syncInterval) clearInterval(syncInterval);
46
+ };
47
+ }, [store, enableServerSync]);
48
+ return /* @__PURE__ */ jsx(
49
+ NextTraceDevTools,
50
+ {
51
+ store,
52
+ defaultPosition,
53
+ defaultTheme,
54
+ initialOpen
55
+ }
56
+ );
57
+ };
58
+ export {
59
+ NextTraceOverlay
60
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * NextTrace Server-Side Outgoing Fetch Collector & Route Handler
3
+ * Captures outgoing HTTP calls made in Server Components, Route Handlers, or Server Actions.
4
+ */
5
+ /**
6
+ * Instruments globalThis.fetch on the Node.js server side in development
7
+ */
8
+ declare function instrumentServerFetch(): () => void;
9
+ /**
10
+ * Next.js App Router Route Handler for `app/api/__nexttrace/route.ts`
11
+ * Exposes server events in dev mode only.
12
+ */
13
+ declare function createNextTraceRouteHandler(): () => Promise<Response>;
14
+
15
+ export { createNextTraceRouteHandler, instrumentServerFetch };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * NextTrace Server-Side Outgoing Fetch Collector & Route Handler
3
+ * Captures outgoing HTTP calls made in Server Components, Route Handlers, or Server Actions.
4
+ */
5
+ /**
6
+ * Instruments globalThis.fetch on the Node.js server side in development
7
+ */
8
+ declare function instrumentServerFetch(): () => void;
9
+ /**
10
+ * Next.js App Router Route Handler for `app/api/__nexttrace/route.ts`
11
+ * Exposes server events in dev mode only.
12
+ */
13
+ declare function createNextTraceRouteHandler(): () => Promise<Response>;
14
+
15
+ export { createNextTraceRouteHandler, instrumentServerFetch };
package/dist/server.js ADDED
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ createNextTraceRouteHandler: () => createNextTraceRouteHandler,
24
+ instrumentServerFetch: () => instrumentServerFetch
25
+ });
26
+ module.exports = __toCommonJS(server_exports);
27
+ var import_core = require("@nexttrace/core");
28
+ var serverEventQueue = [];
29
+ var MAX_SERVER_EVENTS = 100;
30
+ var isServerInstrumented = false;
31
+ var originalGlobalFetch = null;
32
+ function generateEventId() {
33
+ return "srv_" + Math.random().toString(36).substring(2, 10) + Date.now().toString(36);
34
+ }
35
+ function instrumentServerFetch() {
36
+ if (process.env.NODE_ENV !== "development") {
37
+ return () => {
38
+ };
39
+ }
40
+ if (isServerInstrumented || typeof globalThis.fetch === "undefined") {
41
+ return () => {
42
+ };
43
+ }
44
+ originalGlobalFetch = globalThis.fetch;
45
+ isServerInstrumented = true;
46
+ globalThis.fetch = async function(input, init) {
47
+ if (!originalGlobalFetch) return fetch(input, init);
48
+ const urlString = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
49
+ if (urlString.includes("/__nexttrace") || urlString.includes("/_next/")) {
50
+ return originalGlobalFetch.call(globalThis, input, init);
51
+ }
52
+ const eventId = generateEventId();
53
+ const startTime = performance.now();
54
+ const timestamp = Date.now();
55
+ let method = "GET";
56
+ const rawHeaders = {};
57
+ let reqBodyText = null;
58
+ if (init) {
59
+ if (init.method) method = init.method.toUpperCase();
60
+ if (init.headers) {
61
+ if (init.headers instanceof Headers) {
62
+ init.headers.forEach((v, k) => {
63
+ rawHeaders[k] = v;
64
+ });
65
+ } else if (Array.isArray(init.headers)) {
66
+ init.headers.forEach(([k, v]) => {
67
+ rawHeaders[k] = v;
68
+ });
69
+ } else {
70
+ Object.entries(init.headers).forEach(([k, v]) => {
71
+ rawHeaders[k] = String(v);
72
+ });
73
+ }
74
+ }
75
+ if (init.body && typeof init.body === "string") {
76
+ reqBodyText = init.body;
77
+ }
78
+ }
79
+ const { sanitizedUrl, queryParams, wasRedacted: queryRedacted } = (0, import_core.sanitizeUrlAndQuery)(
80
+ urlString,
81
+ import_core.DEFAULT_DENY_QUERY_PARAMS
82
+ );
83
+ const { sanitized: sanitizedReqHeaders, wasRedacted: headersRedacted } = (0, import_core.redactHeaders)(
84
+ rawHeaders,
85
+ import_core.DEFAULT_DENY_HEADERS
86
+ );
87
+ const { preview: reqBodyPreview, wasRedacted: bodyRedacted } = (0, import_core.sanitizeBodyPreview)(
88
+ reqBodyText,
89
+ 25e3,
90
+ import_core.DEFAULT_DENY_JSON_KEYS
91
+ );
92
+ const event = {
93
+ schemaVersion: "1.0.0",
94
+ eventId,
95
+ timestamp,
96
+ phase: "start",
97
+ runtime: "node",
98
+ sourceAdapter: "server-fetch",
99
+ request: {
100
+ method,
101
+ url: urlString,
102
+ sanitizedUrl,
103
+ queryParams,
104
+ headers: sanitizedReqHeaders,
105
+ bodyPreview: reqBodyPreview,
106
+ bodySize: reqBodyText ? reqBodyText.length : 0,
107
+ isTruncated: false
108
+ },
109
+ timing: {
110
+ startTime
111
+ },
112
+ context: {
113
+ attributionMethod: "stack-derived",
114
+ confidence: "medium"
115
+ },
116
+ flags: {
117
+ isInternal: false,
118
+ isRedacted: queryRedacted || headersRedacted || bodyRedacted,
119
+ isTruncated: false,
120
+ isInferred: false
121
+ }
122
+ };
123
+ serverEventQueue.push(event);
124
+ if (serverEventQueue.length > MAX_SERVER_EVENTS) {
125
+ serverEventQueue.shift();
126
+ }
127
+ try {
128
+ const response = await originalGlobalFetch.call(globalThis, input, init);
129
+ const endTime = performance.now();
130
+ const duration = Math.round(endTime - startTime);
131
+ const resHeaders = {};
132
+ response.headers.forEach((v, k) => {
133
+ resHeaders[k] = v;
134
+ });
135
+ const { sanitized: sanitizedResHeaders, wasRedacted: resHeadersRedacted } = (0, import_core.redactHeaders)(
136
+ resHeaders,
137
+ import_core.DEFAULT_DENY_HEADERS
138
+ );
139
+ const contentType = response.headers.get("content-type") || "";
140
+ let resBodyPreview = null;
141
+ if (contentType.includes("application/json") || contentType.includes("text/")) {
142
+ try {
143
+ const clone = response.clone();
144
+ const raw = await clone.text();
145
+ const sanitized = (0, import_core.sanitizeBodyPreview)(raw, 25e3, import_core.DEFAULT_DENY_JSON_KEYS);
146
+ resBodyPreview = sanitized.preview;
147
+ } catch {
148
+ }
149
+ }
150
+ event.phase = response.ok ? "complete" : "error";
151
+ event.timing.endTime = endTime;
152
+ event.timing.duration = duration;
153
+ event.flags.isSlow = duration >= 1e3;
154
+ event.flags.isRedacted = event.flags.isRedacted || resHeadersRedacted;
155
+ event.response = {
156
+ status: response.status,
157
+ statusText: response.statusText,
158
+ headers: sanitizedResHeaders,
159
+ bodyPreview: resBodyPreview,
160
+ contentType,
161
+ bodySize: resBodyPreview ? resBodyPreview.length : 0,
162
+ isTruncated: false
163
+ };
164
+ return response;
165
+ } catch (err) {
166
+ const endTime = performance.now();
167
+ event.phase = "error";
168
+ event.timing.endTime = endTime;
169
+ event.timing.duration = Math.round(endTime - startTime);
170
+ event.error = {
171
+ name: err instanceof Error ? err.name : "ServerError",
172
+ message: err instanceof Error ? err.message : String(err)
173
+ };
174
+ throw err;
175
+ }
176
+ };
177
+ return () => {
178
+ if (isServerInstrumented && originalGlobalFetch) {
179
+ globalThis.fetch = originalGlobalFetch;
180
+ originalGlobalFetch = null;
181
+ isServerInstrumented = false;
182
+ }
183
+ };
184
+ }
185
+ function createNextTraceRouteHandler() {
186
+ return async function GET() {
187
+ if (process.env.NODE_ENV !== "development") {
188
+ return new Response(JSON.stringify({ error: "Forbidden in production" }), {
189
+ status: 403,
190
+ headers: { "Content-Type": "application/json" }
191
+ });
192
+ }
193
+ const events = [...serverEventQueue];
194
+ serverEventQueue.length = 0;
195
+ return new Response(JSON.stringify({ events }), {
196
+ status: 200,
197
+ headers: {
198
+ "Content-Type": "application/json",
199
+ "Cache-Control": "no-store, no-cache",
200
+ "x-nexttrace-internal": "true"
201
+ }
202
+ });
203
+ };
204
+ }
205
+ // Annotate the CommonJS export names for ESM import in node:
206
+ 0 && (module.exports = {
207
+ createNextTraceRouteHandler,
208
+ instrumentServerFetch
209
+ });
@@ -0,0 +1,190 @@
1
+ // src/server.ts
2
+ import {
3
+ redactHeaders,
4
+ sanitizeUrlAndQuery,
5
+ sanitizeBodyPreview,
6
+ DEFAULT_DENY_HEADERS,
7
+ DEFAULT_DENY_QUERY_PARAMS,
8
+ DEFAULT_DENY_JSON_KEYS
9
+ } from "@nexttrace/core";
10
+ var serverEventQueue = [];
11
+ var MAX_SERVER_EVENTS = 100;
12
+ var isServerInstrumented = false;
13
+ var originalGlobalFetch = null;
14
+ function generateEventId() {
15
+ return "srv_" + Math.random().toString(36).substring(2, 10) + Date.now().toString(36);
16
+ }
17
+ function instrumentServerFetch() {
18
+ if (process.env.NODE_ENV !== "development") {
19
+ return () => {
20
+ };
21
+ }
22
+ if (isServerInstrumented || typeof globalThis.fetch === "undefined") {
23
+ return () => {
24
+ };
25
+ }
26
+ originalGlobalFetch = globalThis.fetch;
27
+ isServerInstrumented = true;
28
+ globalThis.fetch = async function(input, init) {
29
+ if (!originalGlobalFetch) return fetch(input, init);
30
+ const urlString = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
31
+ if (urlString.includes("/__nexttrace") || urlString.includes("/_next/")) {
32
+ return originalGlobalFetch.call(globalThis, input, init);
33
+ }
34
+ const eventId = generateEventId();
35
+ const startTime = performance.now();
36
+ const timestamp = Date.now();
37
+ let method = "GET";
38
+ const rawHeaders = {};
39
+ let reqBodyText = null;
40
+ if (init) {
41
+ if (init.method) method = init.method.toUpperCase();
42
+ if (init.headers) {
43
+ if (init.headers instanceof Headers) {
44
+ init.headers.forEach((v, k) => {
45
+ rawHeaders[k] = v;
46
+ });
47
+ } else if (Array.isArray(init.headers)) {
48
+ init.headers.forEach(([k, v]) => {
49
+ rawHeaders[k] = v;
50
+ });
51
+ } else {
52
+ Object.entries(init.headers).forEach(([k, v]) => {
53
+ rawHeaders[k] = String(v);
54
+ });
55
+ }
56
+ }
57
+ if (init.body && typeof init.body === "string") {
58
+ reqBodyText = init.body;
59
+ }
60
+ }
61
+ const { sanitizedUrl, queryParams, wasRedacted: queryRedacted } = sanitizeUrlAndQuery(
62
+ urlString,
63
+ DEFAULT_DENY_QUERY_PARAMS
64
+ );
65
+ const { sanitized: sanitizedReqHeaders, wasRedacted: headersRedacted } = redactHeaders(
66
+ rawHeaders,
67
+ DEFAULT_DENY_HEADERS
68
+ );
69
+ const { preview: reqBodyPreview, wasRedacted: bodyRedacted } = sanitizeBodyPreview(
70
+ reqBodyText,
71
+ 25e3,
72
+ DEFAULT_DENY_JSON_KEYS
73
+ );
74
+ const event = {
75
+ schemaVersion: "1.0.0",
76
+ eventId,
77
+ timestamp,
78
+ phase: "start",
79
+ runtime: "node",
80
+ sourceAdapter: "server-fetch",
81
+ request: {
82
+ method,
83
+ url: urlString,
84
+ sanitizedUrl,
85
+ queryParams,
86
+ headers: sanitizedReqHeaders,
87
+ bodyPreview: reqBodyPreview,
88
+ bodySize: reqBodyText ? reqBodyText.length : 0,
89
+ isTruncated: false
90
+ },
91
+ timing: {
92
+ startTime
93
+ },
94
+ context: {
95
+ attributionMethod: "stack-derived",
96
+ confidence: "medium"
97
+ },
98
+ flags: {
99
+ isInternal: false,
100
+ isRedacted: queryRedacted || headersRedacted || bodyRedacted,
101
+ isTruncated: false,
102
+ isInferred: false
103
+ }
104
+ };
105
+ serverEventQueue.push(event);
106
+ if (serverEventQueue.length > MAX_SERVER_EVENTS) {
107
+ serverEventQueue.shift();
108
+ }
109
+ try {
110
+ const response = await originalGlobalFetch.call(globalThis, input, init);
111
+ const endTime = performance.now();
112
+ const duration = Math.round(endTime - startTime);
113
+ const resHeaders = {};
114
+ response.headers.forEach((v, k) => {
115
+ resHeaders[k] = v;
116
+ });
117
+ const { sanitized: sanitizedResHeaders, wasRedacted: resHeadersRedacted } = redactHeaders(
118
+ resHeaders,
119
+ DEFAULT_DENY_HEADERS
120
+ );
121
+ const contentType = response.headers.get("content-type") || "";
122
+ let resBodyPreview = null;
123
+ if (contentType.includes("application/json") || contentType.includes("text/")) {
124
+ try {
125
+ const clone = response.clone();
126
+ const raw = await clone.text();
127
+ const sanitized = sanitizeBodyPreview(raw, 25e3, DEFAULT_DENY_JSON_KEYS);
128
+ resBodyPreview = sanitized.preview;
129
+ } catch {
130
+ }
131
+ }
132
+ event.phase = response.ok ? "complete" : "error";
133
+ event.timing.endTime = endTime;
134
+ event.timing.duration = duration;
135
+ event.flags.isSlow = duration >= 1e3;
136
+ event.flags.isRedacted = event.flags.isRedacted || resHeadersRedacted;
137
+ event.response = {
138
+ status: response.status,
139
+ statusText: response.statusText,
140
+ headers: sanitizedResHeaders,
141
+ bodyPreview: resBodyPreview,
142
+ contentType,
143
+ bodySize: resBodyPreview ? resBodyPreview.length : 0,
144
+ isTruncated: false
145
+ };
146
+ return response;
147
+ } catch (err) {
148
+ const endTime = performance.now();
149
+ event.phase = "error";
150
+ event.timing.endTime = endTime;
151
+ event.timing.duration = Math.round(endTime - startTime);
152
+ event.error = {
153
+ name: err instanceof Error ? err.name : "ServerError",
154
+ message: err instanceof Error ? err.message : String(err)
155
+ };
156
+ throw err;
157
+ }
158
+ };
159
+ return () => {
160
+ if (isServerInstrumented && originalGlobalFetch) {
161
+ globalThis.fetch = originalGlobalFetch;
162
+ originalGlobalFetch = null;
163
+ isServerInstrumented = false;
164
+ }
165
+ };
166
+ }
167
+ function createNextTraceRouteHandler() {
168
+ return async function GET() {
169
+ if (process.env.NODE_ENV !== "development") {
170
+ return new Response(JSON.stringify({ error: "Forbidden in production" }), {
171
+ status: 403,
172
+ headers: { "Content-Type": "application/json" }
173
+ });
174
+ }
175
+ const events = [...serverEventQueue];
176
+ serverEventQueue.length = 0;
177
+ return new Response(JSON.stringify({ events }), {
178
+ status: 200,
179
+ headers: {
180
+ "Content-Type": "application/json",
181
+ "Cache-Control": "no-store, no-cache",
182
+ "x-nexttrace-internal": "true"
183
+ }
184
+ });
185
+ };
186
+ }
187
+ export {
188
+ createNextTraceRouteHandler,
189
+ instrumentServerFetch
190
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@nexttrace/next",
3
+ "version": "0.1.0",
4
+ "description": "Next.js integration for NextTrace with dev-only gating, server fetch collection, and overlay component",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./server": {
15
+ "types": "./dist/server.d.ts",
16
+ "import": "./dist/server.mjs",
17
+ "require": "./dist/server.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "keywords": [
24
+ "nextjs",
25
+ "app-router",
26
+ "devtools",
27
+ "nexttrace",
28
+ "tracing"
29
+ ],
30
+ "author": "NextTrace Team",
31
+ "license": "MIT",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "dependencies": {
36
+ "@nexttrace/core": "0.1.0",
37
+ "@nexttrace/devtools": "0.1.0"
38
+ },
39
+ "peerDependencies": {
40
+ "next": ">=13.0.0 || >=14.0.0 || >=15.0.0",
41
+ "react": ">=18.0.0 || >=19.0.0",
42
+ "react-dom": ">=18.0.0 || >=19.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20.14.0",
46
+ "@types/react": "^18.3.5",
47
+ "@types/react-dom": "^18.3.0",
48
+ "next": "^14.2.13",
49
+ "react": "^18.3.1",
50
+ "react-dom": "^18.3.1",
51
+ "tsup": "^8.2.4",
52
+ "typescript": "^5.5.4"
53
+ },
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "dev": "tsup --watch",
57
+ "lint": "tsc --noEmit",
58
+ "typecheck": "tsc --noEmit"
59
+ }
60
+ }