@hawk.so/nextjs 1.0.0 → 1.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/dist/server.js ADDED
@@ -0,0 +1,452 @@
1
+ // src/server/sdk.ts
2
+ import { isLoggerSet, log, setLogger } from "@hawk.so/core";
3
+
4
+ // src/common/carrier.ts
5
+ var CARRIER_KEY = /* @__PURE__ */ Symbol.for("hawk.nextjs");
6
+ function getCarrier() {
7
+ const store = globalThis;
8
+ store[CARRIER_KEY] ??= {};
9
+ return store[CARRIER_KEY];
10
+ }
11
+
12
+ // src/common/control-flow.ts
13
+ var CONTROL_FLOW_DIGEST_PREFIXES = [
14
+ "NEXT_REDIRECT",
15
+ "NEXT_HTTP_ERROR_FALLBACK;",
16
+ "NEXT_NOT_FOUND",
17
+ "BAILOUT_TO_CLIENT_SIDE_RENDERING",
18
+ "DYNAMIC_SERVER_USAGE",
19
+ "NEXT_PRERENDER_INTERRUPTED",
20
+ "HANGING_PROMISE_REJECTION"
21
+ ];
22
+ var STATIC_GEN_BAILOUT_CODE = "NEXT_STATIC_GEN_BAILOUT";
23
+ var REACT_POSTPONE_TYPE = /* @__PURE__ */ Symbol.for("react.postpone");
24
+ var MAX_CAUSE_DEPTH = 5;
25
+ function isControlFlowDigest(value) {
26
+ return typeof value === "string" && CONTROL_FLOW_DIGEST_PREFIXES.some((prefix) => value.startsWith(prefix));
27
+ }
28
+ function isNextControlFlowError(error) {
29
+ let current = error;
30
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH; depth++) {
31
+ if (typeof current !== "object" || current === null) {
32
+ return false;
33
+ }
34
+ const value = current;
35
+ if (isControlFlowDigest(value.digest) || isControlFlowDigest(value.message) || value.code === STATIC_GEN_BAILOUT_CODE || value.$$typeof === REACT_POSTPONE_TYPE) {
36
+ return true;
37
+ }
38
+ current = value.cause;
39
+ }
40
+ return false;
41
+ }
42
+ function getDigest(error) {
43
+ if (typeof error === "object" && error !== null) {
44
+ const { digest } = error;
45
+ if (typeof digest === "string" && digest !== "") {
46
+ return digest;
47
+ }
48
+ }
49
+ return void 0;
50
+ }
51
+
52
+ // src/common/error.ts
53
+ function isErrorLike(value) {
54
+ return typeof value === "object" && value !== null && typeof value.message === "string" && typeof value.stack === "string";
55
+ }
56
+ function normalizeError(value) {
57
+ if (value instanceof Error || typeof value === "string") {
58
+ return value;
59
+ }
60
+ if (isErrorLike(value)) {
61
+ const error = new Error(value.message);
62
+ error.name = typeof value.name === "string" ? value.name : error.name;
63
+ error.stack = value.stack;
64
+ return error;
65
+ }
66
+ try {
67
+ return JSON.stringify(value) ?? String(value);
68
+ } catch (_error) {
69
+ return String(value);
70
+ }
71
+ }
72
+
73
+ // src/common/options.ts
74
+ var DEFAULT_FLUSH_TIMEOUT = 2e3;
75
+
76
+ // src/common/request.ts
77
+ var SAFE_HEADERS = ["host", "referer", "user-agent"];
78
+ function pickSafeHeaders(headers) {
79
+ const result = {};
80
+ for (const name of SAFE_HEADERS) {
81
+ const value = headers[name];
82
+ if (value !== void 0) {
83
+ result[name] = Array.isArray(value) ? value.join(", ") : value;
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+ function getRequestAddons(error, request, context) {
89
+ const addons = {
90
+ digest: getDigest(error),
91
+ routerKind: context.routerKind,
92
+ routePath: context.routePath,
93
+ routeType: context.routeType,
94
+ renderSource: context.renderSource,
95
+ revalidateReason: context.revalidateReason,
96
+ renderType: context.renderType,
97
+ method: request.method,
98
+ path: request.path,
99
+ headers: pickSafeHeaders(request.headers)
100
+ };
101
+ return Object.fromEntries(Object.entries(addons).filter(([, value]) => value !== void 0));
102
+ }
103
+
104
+ // src/server/catcher.ts
105
+ import { BaseCatcher, decodeIntegrationId, HawkUserManager } from "@hawk.so/core";
106
+
107
+ // src/common/timeout.ts
108
+ async function settleWithTimeout(promises, timeout) {
109
+ const list = [...promises];
110
+ if (list.length === 0) {
111
+ return true;
112
+ }
113
+ let timer;
114
+ const timeoutReached = new Promise((resolve) => {
115
+ timer = setTimeout(() => resolve(false), timeout);
116
+ });
117
+ const allSettled = Promise.allSettled(list).then(() => true);
118
+ try {
119
+ return await Promise.race([allSettled, timeoutReached]);
120
+ } finally {
121
+ clearTimeout(timer);
122
+ }
123
+ }
124
+
125
+ // src/common/version.ts
126
+ var VERSION = true ? "1.1.0" : "dev";
127
+
128
+ // src/server/stack-parser.ts
129
+ import { StackParser } from "@hawk.so/core";
130
+ import ErrorStackParser from "error-stack-parser";
131
+ var ServerStackParser = class extends StackParser {
132
+ /**
133
+ * Returns frames of the stack trace
134
+ * @param error - error with the stack trace
135
+ */
136
+ parse(error) {
137
+ const frames = ErrorStackParser.parse(error).map((frame) => ({
138
+ file: frame.fileName ?? "",
139
+ line: frame.lineNumber ?? 0,
140
+ column: frame.columnNumber,
141
+ function: frame.functionName,
142
+ arguments: frame.args
143
+ }));
144
+ return Promise.resolve(frames);
145
+ }
146
+ };
147
+
148
+ // src/server/transport.ts
149
+ var REQUEST_TIMEOUT = 1e4;
150
+ var FetchTransport = class {
151
+ /**
152
+ * Collector endpoint
153
+ */
154
+ endpoint;
155
+ /**
156
+ * Requests in progress
157
+ */
158
+ pending = /* @__PURE__ */ new Set();
159
+ /**
160
+ * @param endpoint - collector endpoint, e.g. `https://<integration id>.k1.hawk.so/`
161
+ */
162
+ constructor(endpoint) {
163
+ this.endpoint = endpoint;
164
+ }
165
+ /**
166
+ * Sends the event
167
+ * @param message - event with the token and the catcher type
168
+ */
169
+ send(message) {
170
+ const request = this.post(message).finally(() => {
171
+ this.pending.delete(request);
172
+ });
173
+ this.pending.add(request);
174
+ return request;
175
+ }
176
+ /**
177
+ * Waits for the requests in progress
178
+ * @param timeout - max time to wait, in milliseconds
179
+ * @returns false when the timeout is reached
180
+ */
181
+ flush(timeout) {
182
+ return settleWithTimeout(this.pending, timeout);
183
+ }
184
+ /**
185
+ * Posts the event to the collector
186
+ * @param message - event with the token and the catcher type
187
+ */
188
+ async post(message) {
189
+ const controller = new AbortController();
190
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
191
+ try {
192
+ const response = await fetch(this.endpoint, {
193
+ method: "POST",
194
+ headers: [["Content-Type", "application/json"]],
195
+ body: JSON.stringify(message),
196
+ signal: controller.signal
197
+ });
198
+ if (!response.ok) {
199
+ throw new Error(`Collector responded with ${response.status} ${response.statusText}`);
200
+ }
201
+ } finally {
202
+ clearTimeout(timer);
203
+ }
204
+ }
205
+ };
206
+
207
+ // src/server/catcher.ts
208
+ var usersByError = /* @__PURE__ */ new WeakMap();
209
+ var noopStorage = {
210
+ getItem: () => null,
211
+ setItem: () => void 0,
212
+ removeItem: () => void 0
213
+ };
214
+ var zeroRandomGenerator = {
215
+ getRandomNumbers: (length) => new Uint8Array(length)
216
+ };
217
+ var ServerUserManager = class extends HawkUserManager {
218
+ /**
219
+ * Creates a manager that never stores the user
220
+ */
221
+ constructor() {
222
+ super(noopStorage, zeroRandomGenerator);
223
+ }
224
+ };
225
+ var ServerUserProcessor = class {
226
+ /**
227
+ * @param payload - event being built
228
+ * @param error - sent error
229
+ */
230
+ apply(payload, error) {
231
+ const user = typeof error === "object" ? usersByError.get(error) : void 0;
232
+ if (user !== void 0) {
233
+ payload.user = user;
234
+ } else {
235
+ delete payload.user;
236
+ }
237
+ return payload;
238
+ }
239
+ };
240
+ function getDefaultEndpoint(token) {
241
+ return `https://${decodeIntegrationId(token)}.k1.hawk.so/`;
242
+ }
243
+ var ServerCatcher = class extends BaseCatcher {
244
+ /**
245
+ * Transport kept to wait for the events in progress
246
+ */
247
+ fetchTransport;
248
+ /**
249
+ * Current runtime: `nodejs` or `edge`
250
+ */
251
+ runtime;
252
+ /**
253
+ * Events being built: the stack trace is parsed asynchronously before the event gets to the transport
254
+ */
255
+ pending = /* @__PURE__ */ new Set();
256
+ /**
257
+ * @param settings - token, release and other settings
258
+ * @throws when the token is invalid and the collector endpoint is not set
259
+ */
260
+ constructor(settings) {
261
+ const transport = new FetchTransport(settings.collectorEndpoint ?? getDefaultEndpoint(settings.token));
262
+ super(settings.token, transport, new ServerUserManager(), settings.release, settings.context, settings.beforeSend);
263
+ this.fetchTransport = transport;
264
+ this.runtime = settings.runtime;
265
+ this.addMessageProcessor(new ServerUserProcessor());
266
+ this.stackParser = new ServerStackParser();
267
+ }
268
+ /**
269
+ * Sends the error and resolves when it is passed to the transport
270
+ * @param error - error or message
271
+ * @param options - context, user and Next.js data of the event
272
+ */
273
+ capture(error, options = {}) {
274
+ if (options.user !== void 0 && typeof error === "object") {
275
+ usersByError.set(error, options.user);
276
+ }
277
+ const addons = {
278
+ runtime: this.runtime,
279
+ ...options.addons
280
+ };
281
+ const building = this.formatAndSend(error, { nextjs: addons }, options.context).finally(() => {
282
+ this.pending.delete(building);
283
+ });
284
+ this.pending.add(building);
285
+ return building;
286
+ }
287
+ /**
288
+ * Waits for the events being built and sent
289
+ * @param timeout - max time to wait, in milliseconds
290
+ * @returns false when the timeout is reached
291
+ */
292
+ async flush(timeout) {
293
+ const deadline = Date.now() + timeout;
294
+ if (!await settleWithTimeout(this.pending, timeout)) {
295
+ return false;
296
+ }
297
+ return this.fetchTransport.flush(Math.max(deadline - Date.now(), 0));
298
+ }
299
+ /**
300
+ * Returns the catcher type
301
+ */
302
+ getCatcherType() {
303
+ return "errors/nodejs";
304
+ }
305
+ /**
306
+ * Returns the catcher version
307
+ */
308
+ getCatcherVersion() {
309
+ return VERSION;
310
+ }
311
+ };
312
+
313
+ // src/server/sdk.ts
314
+ var REQUEST_CONTEXT_KEY = /* @__PURE__ */ Symbol.for("@next/request-context");
315
+ var noopBreadcrumbs = {
316
+ add: () => void 0,
317
+ get: () => [],
318
+ clear: () => void 0
319
+ };
320
+ var isSetUserWarningShown = false;
321
+ function getCatcher() {
322
+ return getCarrier().catcher;
323
+ }
324
+ function setConsoleLogger() {
325
+ if (isLoggerSet()) {
326
+ return;
327
+ }
328
+ setLogger((message, type = "log", args) => {
329
+ const method = type === "warn" || type === "error" || type === "info" ? type : "log";
330
+ const text = `\u{1F985} Hawk | ${message}`;
331
+ if (args === void 0) {
332
+ console[method](text);
333
+ } else {
334
+ console[method](text, args);
335
+ }
336
+ });
337
+ }
338
+ function getEnvToken() {
339
+ return [process.env.HAWK_TOKEN, process.env.NEXT_PUBLIC_HAWK_TOKEN].find((value) => value !== void 0 && value !== "");
340
+ }
341
+ function waitUntil(promise) {
342
+ globalThis[REQUEST_CONTEXT_KEY]?.get?.()?.waitUntil?.(promise);
343
+ }
344
+ function initServerCatcher(options, runtime) {
345
+ const carrier = getCarrier();
346
+ if (carrier.catcher !== void 0) {
347
+ return void 0;
348
+ }
349
+ setConsoleLogger();
350
+ const token = options.token ?? getEnvToken();
351
+ if (token === void 0 || token === "") {
352
+ log("Integration Token is not provided, errors will not be sent. Set NEXT_PUBLIC_HAWK_TOKEN or pass `token` to init()", "warn");
353
+ return void 0;
354
+ }
355
+ try {
356
+ carrier.catcher = new ServerCatcher({
357
+ token,
358
+ release: options.release ?? process.env.HAWK_RELEASE,
359
+ context: options.context,
360
+ beforeSend: options.beforeSend,
361
+ collectorEndpoint: options.collectorEndpoint,
362
+ runtime
363
+ });
364
+ } catch (_error) {
365
+ log("Invalid Integration Token, errors will not be sent", "error");
366
+ return void 0;
367
+ }
368
+ const { catcher } = carrier;
369
+ carrier.capture = (error, addons) => {
370
+ void catcher.capture(normalizeError(error), { addons });
371
+ };
372
+ return catcher;
373
+ }
374
+ async function captureServerError(error, options = {}) {
375
+ const catcher = getCatcher();
376
+ if (catcher === void 0 || isNextControlFlowError(error)) {
377
+ return;
378
+ }
379
+ await catcher.capture(normalizeError(error), options);
380
+ }
381
+ function send(error, context, user) {
382
+ void captureServerError(error, {
383
+ context,
384
+ user
385
+ });
386
+ }
387
+ function setUser(_user) {
388
+ if (!isSetUserWarningShown) {
389
+ isSetUserWarningShown = true;
390
+ log("setUser() is ignored on the server, pass the user to send() instead", "warn");
391
+ }
392
+ }
393
+ function clearUser() {
394
+ }
395
+ function setContext(context) {
396
+ getCatcher()?.setContext(context);
397
+ }
398
+ var breadcrumbs = noopBreadcrumbs;
399
+ async function flush(timeout = DEFAULT_FLUSH_TIMEOUT) {
400
+ const catcher = getCatcher();
401
+ return catcher === void 0 ? false : catcher.flush(timeout);
402
+ }
403
+ async function onRequestError(error, request, context) {
404
+ const catcher = getCatcher();
405
+ if (catcher === void 0 || isNextControlFlowError(error)) {
406
+ return;
407
+ }
408
+ const sending = captureServerError(error, {
409
+ addons: getRequestAddons(error, request, context)
410
+ }).then(() => catcher.flush(DEFAULT_FLUSH_TIMEOUT));
411
+ waitUntil(sending);
412
+ await sending;
413
+ }
414
+ function onRouterTransitionStart(_url, _navigationType) {
415
+ }
416
+
417
+ // src/server/index.ts
418
+ import { HawkErrorBoundary, HawkGlobalError, useHawkError } from "./react.js";
419
+ async function handleProcessError(event, error) {
420
+ await captureServerError(error);
421
+ await flush();
422
+ if (process.listenerCount(event) === 1) {
423
+ console.error(error);
424
+ process.exit(1);
425
+ }
426
+ }
427
+ function init(options = {}) {
428
+ const catcher = initServerCatcher(options, "nodejs");
429
+ if (catcher === void 0 || options.disableGlobalErrorsHandling === true) {
430
+ return;
431
+ }
432
+ process.on("uncaughtException", (error) => {
433
+ void handleProcessError("uncaughtException", error);
434
+ });
435
+ process.on("unhandledRejection", (reason) => {
436
+ void handleProcessError("unhandledRejection", reason);
437
+ });
438
+ }
439
+ export {
440
+ HawkErrorBoundary,
441
+ HawkGlobalError,
442
+ breadcrumbs,
443
+ clearUser,
444
+ flush,
445
+ init,
446
+ onRequestError,
447
+ onRouterTransitionStart,
448
+ send,
449
+ setContext,
450
+ setUser,
451
+ useHawkError
452
+ };
package/package.json CHANGED
@@ -1,30 +1,39 @@
1
1
  {
2
2
  "name": "@hawk.so/nextjs",
3
- "version": "1.0.0",
4
- "description": "Next.js integration for sending releases with source maps to Hawk",
3
+ "version": "1.1.0",
4
+ "description": "Hawk error tracking for Next.js: catches errors in the browser, Node.js and Edge, sends releases with source maps",
5
5
  "keywords": [
6
6
  "hawk",
7
7
  "nextjs",
8
+ "error tracking",
8
9
  "turbopack",
9
- "source maps",
10
- "error tracking"
10
+ "source maps"
11
11
  ],
12
12
  "repository": "https://github.com/codex-team/hawk.nextjs.git",
13
13
  "author": "CodeX <team@codex.so>",
14
14
  "license": "MIT",
15
15
  "type": "module",
16
- "main": "./dist/index.cjs",
17
- "module": "./dist/index.js",
16
+ "main": "./dist/server.cjs",
17
+ "module": "./dist/server.js",
18
18
  "types": "./dist/index.d.ts",
19
19
  "exports": {
20
20
  ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "edge-light": "./dist/edge.js",
23
+ "worker": "./dist/edge.js",
24
+ "workerd": "./dist/edge.js",
25
+ "browser": "./dist/client.js",
26
+ "import": "./dist/server.js",
27
+ "require": "./dist/server.cjs"
28
+ },
29
+ "./config": {
21
30
  "import": {
22
- "types": "./dist/index.d.ts",
23
- "default": "./dist/index.js"
31
+ "types": "./dist/config.d.ts",
32
+ "default": "./dist/config.js"
24
33
  },
25
34
  "require": {
26
- "types": "./dist/index.d.cts",
27
- "default": "./dist/index.cjs"
35
+ "types": "./dist/config.d.cts",
36
+ "default": "./dist/config.cjs"
28
37
  }
29
38
  },
30
39
  "./global": {
@@ -36,6 +45,7 @@
36
45
  "dist",
37
46
  "global.d.ts"
38
47
  ],
48
+ "sideEffects": false,
39
49
  "workspaces": [
40
50
  "example"
41
51
  ],
@@ -43,7 +53,7 @@
43
53
  "node": ">=20.9.0"
44
54
  },
45
55
  "scripts": {
46
- "build": "tsup",
56
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsup",
47
57
  "lint": "eslint .",
48
58
  "lint:fix": "eslint . --fix",
49
59
  "typecheck": "tsc --noEmit",
@@ -52,19 +62,34 @@
52
62
  "e2e": "yarn build && node scripts/e2e.mjs",
53
63
  "prepublishOnly": "yarn build"
54
64
  },
65
+ "dependencies": {
66
+ "@hawk.so/browser": "^3.4.0",
67
+ "@hawk.so/core": "^1.0.0",
68
+ "@hawk.so/types": "^0.5.8",
69
+ "error-stack-parser": "^2.1.4"
70
+ },
55
71
  "peerDependencies": {
56
- "next": ">=15.4.1"
72
+ "next": ">=15.4.1",
73
+ "react": "^18.2.0 || ^19.0.0"
57
74
  },
58
75
  "devDependencies": {
59
76
  "@types/node": "^24.19.2",
77
+ "@types/react": "^19.3.0",
78
+ "@types/react-dom": "^19.2.0",
79
+ "@types/ws": "^8.18.2",
60
80
  "eslint": "^9.39.5",
61
81
  "eslint-config-codex": "^2.0.3",
82
+ "happy-dom": "^21.0.4",
62
83
  "next": "^16.4.3",
84
+ "playwright": "^1.64.1",
85
+ "react": "^19.3.1",
86
+ "react-dom": "^19.3.1",
63
87
  "tsup": "^9.0.0",
64
88
  "typescript": "^5.9.3",
65
89
  "typescript-eslint": "^8.70.0",
66
90
  "vite": "^8.0.0",
67
- "vitest": "^5.0.5"
91
+ "vitest": "^5.0.5",
92
+ "ws": "^8.22.0"
68
93
  },
69
94
  "packageManager": "yarn@4.18.1"
70
95
  }