@beignet/devtools 0.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +464 -0
  3. package/dist/access.d.ts +21 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +20 -0
  6. package/dist/access.js.map +1 -0
  7. package/dist/audit.d.ts +10 -0
  8. package/dist/audit.d.ts.map +1 -0
  9. package/dist/audit.js +49 -0
  10. package/dist/audit.js.map +1 -0
  11. package/dist/events.d.ts +143 -0
  12. package/dist/events.d.ts.map +1 -0
  13. package/dist/events.js +20 -0
  14. package/dist/events.js.map +1 -0
  15. package/dist/index.d.ts +114 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +44 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/instrumentation.d.ts +74 -0
  20. package/dist/instrumentation.d.ts.map +1 -0
  21. package/dist/instrumentation.js +293 -0
  22. package/dist/instrumentation.js.map +1 -0
  23. package/dist/persistence.d.ts +30 -0
  24. package/dist/persistence.d.ts.map +1 -0
  25. package/dist/persistence.js +100 -0
  26. package/dist/persistence.js.map +1 -0
  27. package/dist/provider-instrumentation.d.ts +9 -0
  28. package/dist/provider-instrumentation.d.ts.map +1 -0
  29. package/dist/provider-instrumentation.js +25 -0
  30. package/dist/provider-instrumentation.js.map +1 -0
  31. package/dist/provider.d.ts +79 -0
  32. package/dist/provider.d.ts.map +1 -0
  33. package/dist/provider.js +293 -0
  34. package/dist/provider.js.map +1 -0
  35. package/dist/redaction.d.ts +5 -0
  36. package/dist/redaction.d.ts.map +1 -0
  37. package/dist/redaction.js +20 -0
  38. package/dist/redaction.js.map +1 -0
  39. package/dist/routes.d.ts +113 -0
  40. package/dist/routes.d.ts.map +1 -0
  41. package/dist/routes.js +247 -0
  42. package/dist/routes.js.map +1 -0
  43. package/dist/trace-context.d.ts +29 -0
  44. package/dist/trace-context.d.ts.map +1 -0
  45. package/dist/trace-context.js +74 -0
  46. package/dist/trace-context.js.map +1 -0
  47. package/dist/ui.d.ts +14 -0
  48. package/dist/ui.d.ts.map +1 -0
  49. package/dist/ui.js +795 -0
  50. package/dist/ui.js.map +1 -0
  51. package/dist/watchers.d.ts +22 -0
  52. package/dist/watchers.d.ts.map +1 -0
  53. package/dist/watchers.js +171 -0
  54. package/dist/watchers.js.map +1 -0
  55. package/package.json +66 -0
  56. package/src/access.ts +52 -0
  57. package/src/audit.ts +71 -0
  58. package/src/events.ts +193 -0
  59. package/src/index.ts +136 -0
  60. package/src/instrumentation.ts +451 -0
  61. package/src/persistence.ts +163 -0
  62. package/src/provider-instrumentation.ts +50 -0
  63. package/src/provider.ts +375 -0
  64. package/src/redaction.ts +26 -0
  65. package/src/routes.ts +317 -0
  66. package/src/trace-context.ts +115 -0
  67. package/src/ui.ts +807 -0
  68. package/src/watchers.ts +235 -0
@@ -0,0 +1,163 @@
1
+ import type { DevtoolsEvent } from "./events";
2
+
3
+ export interface DevtoolsEventStore {
4
+ name?: string;
5
+ load?(): DevtoolsEvent[] | Promise<DevtoolsEvent[]>;
6
+ append?(
7
+ event: DevtoolsEvent,
8
+ events: readonly DevtoolsEvent[],
9
+ ): void | Promise<void>;
10
+ clear?(): void | Promise<void>;
11
+ }
12
+
13
+ export interface FileDevtoolsStoreOptions {
14
+ /**
15
+ * JSONL file used for persisted devtools events.
16
+ *
17
+ * @default ".beignet/devtools/core/events.jsonl"
18
+ */
19
+ filePath?: string;
20
+
21
+ /**
22
+ * Maximum number of events to load and keep when compacting the file.
23
+ *
24
+ * @default 1000
25
+ */
26
+ maxEvents?: number;
27
+
28
+ /**
29
+ * Rewrite the JSONL file after this many appended events. Compaction keeps
30
+ * persistence bounded without making every record pay the rewrite cost.
31
+ *
32
+ * @default 50
33
+ */
34
+ compactEvery?: number;
35
+ }
36
+
37
+ const DEFAULT_FILE_PATH = ".beignet/devtools/core/events.jsonl";
38
+ const DEFAULT_MAX_EVENTS = 1000;
39
+ const DEFAULT_COMPACT_EVERY = 50;
40
+
41
+ type NodeFs = typeof import("node:fs/promises");
42
+ type NodePath = typeof import("node:path");
43
+
44
+ function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
45
+ return (
46
+ typeof error === "object" &&
47
+ error !== null &&
48
+ "code" in error &&
49
+ typeof (error as { code?: unknown }).code === "string"
50
+ );
51
+ }
52
+
53
+ function isDevtoolsEvent(value: unknown): value is DevtoolsEvent {
54
+ return (
55
+ typeof value === "object" &&
56
+ value !== null &&
57
+ "id" in value &&
58
+ "timestamp" in value &&
59
+ "type" in value
60
+ );
61
+ }
62
+
63
+ function parseJsonLine(line: string): DevtoolsEvent | undefined {
64
+ try {
65
+ const parsed = JSON.parse(line);
66
+ return isDevtoolsEvent(parsed) ? parsed : undefined;
67
+ } catch {
68
+ return undefined;
69
+ }
70
+ }
71
+
72
+ async function loadNodeModules(): Promise<{
73
+ fs: NodeFs;
74
+ path: NodePath;
75
+ }> {
76
+ const [fs, path] = await Promise.all([
77
+ import("node:fs/promises"),
78
+ import("node:path"),
79
+ ]);
80
+ return { fs, path };
81
+ }
82
+
83
+ export function createFileDevtoolsStore(
84
+ options: FileDevtoolsStoreOptions = {},
85
+ ): DevtoolsEventStore {
86
+ const filePath = options.filePath ?? DEFAULT_FILE_PATH;
87
+ const maxEvents = Math.max(1, options.maxEvents ?? DEFAULT_MAX_EVENTS);
88
+ const compactEvery = Math.max(
89
+ 1,
90
+ options.compactEvery ?? DEFAULT_COMPACT_EVERY,
91
+ );
92
+ let appendCount = 0;
93
+ let queue = Promise.resolve();
94
+
95
+ async function ensureDirectory(fs: NodeFs, path: NodePath): Promise<void> {
96
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
97
+ }
98
+
99
+ async function readEvents(): Promise<DevtoolsEvent[]> {
100
+ const { fs } = await loadNodeModules();
101
+ let content: string;
102
+
103
+ try {
104
+ content = await fs.readFile(filePath, "utf8");
105
+ } catch (error) {
106
+ if (isErrnoException(error) && error.code === "ENOENT") return [];
107
+ throw error;
108
+ }
109
+
110
+ return content
111
+ .split("\n")
112
+ .map((line) => line.trim())
113
+ .filter(Boolean)
114
+ .map(parseJsonLine)
115
+ .filter((event): event is DevtoolsEvent => event !== undefined)
116
+ .slice(-maxEvents);
117
+ }
118
+
119
+ async function writeEvents(events: readonly DevtoolsEvent[]): Promise<void> {
120
+ const { fs, path } = await loadNodeModules();
121
+ await ensureDirectory(fs, path);
122
+ const content = events.map((event) => JSON.stringify(event)).join("\n");
123
+ await fs.writeFile(filePath, content ? `${content}\n` : "", "utf8");
124
+ }
125
+
126
+ function enqueue(task: () => Promise<void>): Promise<void> {
127
+ queue = queue.then(task, task);
128
+ return queue;
129
+ }
130
+
131
+ return {
132
+ name: "file",
133
+ async load() {
134
+ const events = await readEvents();
135
+ await writeEvents(events);
136
+ return events;
137
+ },
138
+ append(event, events) {
139
+ appendCount += 1;
140
+ const currentAppendCount = appendCount;
141
+
142
+ return enqueue(async () => {
143
+ const shouldCompact =
144
+ currentAppendCount % compactEvery === 0 || events.length >= maxEvents;
145
+
146
+ if (shouldCompact) {
147
+ await writeEvents(events.slice(-maxEvents));
148
+ return;
149
+ }
150
+
151
+ const { fs, path } = await loadNodeModules();
152
+ await ensureDirectory(fs, path);
153
+ await fs.appendFile(filePath, `${JSON.stringify(event)}\n`, "utf8");
154
+ });
155
+ },
156
+ clear() {
157
+ appendCount = 0;
158
+ return enqueue(async () => {
159
+ await writeEvents([]);
160
+ });
161
+ },
162
+ };
163
+ }
@@ -0,0 +1,50 @@
1
+ import {
2
+ createProviderInstrumentation,
3
+ isProviderInstrumentationPort,
4
+ type ProviderCustomInstrumentationEventInput,
5
+ type ProviderInstrumentation,
6
+ type ProviderInstrumentationOptions,
7
+ } from "@beignet/core/providers";
8
+ import type { DevtoolsPort } from "./index";
9
+
10
+ export type ProviderDevtoolsOptions = ProviderInstrumentationOptions;
11
+ export type ProviderCustomDevtoolsEventInput =
12
+ ProviderCustomInstrumentationEventInput;
13
+ export type ProviderDevtools = ProviderInstrumentation;
14
+
15
+ export function isDevtoolsPort(value: unknown): value is DevtoolsPort {
16
+ return (
17
+ isProviderInstrumentationPort(value) &&
18
+ typeof value === "object" &&
19
+ value !== null &&
20
+ "getEvents" in value &&
21
+ typeof value.getEvents === "function" &&
22
+ "isWatcherEnabled" in value &&
23
+ typeof value.isWatcherEnabled === "function"
24
+ );
25
+ }
26
+
27
+ export function resolveDevtoolsPort(target: unknown): DevtoolsPort | undefined {
28
+ if (isDevtoolsPort(target)) return target;
29
+
30
+ if (
31
+ typeof target === "object" &&
32
+ target !== null &&
33
+ "devtools" in target &&
34
+ isDevtoolsPort(target.devtools)
35
+ ) {
36
+ return target.devtools;
37
+ }
38
+
39
+ return undefined;
40
+ }
41
+
42
+ export function createProviderDevtools(
43
+ target: unknown,
44
+ options: ProviderDevtoolsOptions,
45
+ ): ProviderDevtools {
46
+ return createProviderInstrumentation(
47
+ resolveDevtoolsPort(target) ?? undefined,
48
+ options,
49
+ );
50
+ }
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Devtools provider implementation
3
+ *
4
+ * Provides an in-memory event buffer that can be attached to ctx.ports.devtools.
5
+ * The provider is disabled by default in production environments.
6
+ */
7
+
8
+ import { createProvider } from "@beignet/core/providers";
9
+ import { z } from "zod";
10
+ import type {
11
+ DevtoolsEvent,
12
+ DevtoolsEventInput,
13
+ DevtoolsListener,
14
+ DevtoolsRedactor,
15
+ DevtoolsSubscriptionEvent,
16
+ } from "./events";
17
+ import type { DevtoolsFilter, DevtoolsPort } from "./index";
18
+ import {
19
+ createFileDevtoolsStore,
20
+ type DevtoolsEventStore,
21
+ } from "./persistence";
22
+ import {
23
+ applyDevtoolsRedaction,
24
+ createRedactionFailureEvent,
25
+ } from "./redaction";
26
+ import {
27
+ type DevtoolsWatcher,
28
+ type DevtoolsWatchersOptions,
29
+ isDevtoolsEventEnabled,
30
+ isDevtoolsWatcherEnabled,
31
+ resolveDevtoolsWatchers,
32
+ } from "./watchers";
33
+
34
+ /**
35
+ * Maximum number of events to keep in the buffer.
36
+ * When this limit is reached, the oldest events are removed.
37
+ */
38
+ const MAX_EVENTS = 500;
39
+
40
+ export interface InMemoryDevtoolsOptions {
41
+ maxEvents?: number;
42
+ redact?: DevtoolsRedactor;
43
+ watchers?: DevtoolsWatchersOptions;
44
+ initialEvents?: readonly DevtoolsEvent[];
45
+ store?: DevtoolsEventStore;
46
+ }
47
+
48
+ function createEventId(): string {
49
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
50
+ return crypto.randomUUID();
51
+ }
52
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
53
+ }
54
+
55
+ export function createDevtoolsEvent(event: DevtoolsEventInput): DevtoolsEvent {
56
+ return {
57
+ ...event,
58
+ id: event.id ?? createEventId(),
59
+ timestamp: event.timestamp ?? new Date().toISOString(),
60
+ } as DevtoolsEvent;
61
+ }
62
+
63
+ /**
64
+ * Create an in-memory devtools implementation.
65
+ *
66
+ * This implementation stores events in a bounded buffer (max 500 events).
67
+ * When the buffer is full, the oldest events are removed to make room for new ones.
68
+ *
69
+ * @returns A DevtoolsPort implementation
70
+ */
71
+ export function createInMemoryDevtools(
72
+ options: InMemoryDevtoolsOptions = {},
73
+ ): DevtoolsPort {
74
+ const listeners = new Set<DevtoolsListener>();
75
+ const maxEvents = options.maxEvents ?? MAX_EVENTS;
76
+ const watchers = resolveDevtoolsWatchers(options.watchers);
77
+ const events: DevtoolsEvent[] = [];
78
+
79
+ function notify(event: DevtoolsSubscriptionEvent): void {
80
+ for (const listener of listeners) {
81
+ try {
82
+ listener(event);
83
+ } catch {
84
+ // Subscribers are observers; they must not affect devtools storage.
85
+ }
86
+ }
87
+ }
88
+
89
+ function getErrorMessage(error: unknown): string {
90
+ if (error instanceof Error) return error.message;
91
+ if (typeof error === "string") return error;
92
+ return "Unknown error";
93
+ }
94
+
95
+ function persistEvent(event: DevtoolsEvent): void {
96
+ if (!options.store?.append) return;
97
+
98
+ void Promise.resolve(options.store.append(event, [...events])).catch(
99
+ (error) => {
100
+ storeInternalError({
101
+ type: "error",
102
+ message: "Devtools persistence failed",
103
+ details: {
104
+ message: getErrorMessage(error),
105
+ store: options.store?.name,
106
+ },
107
+ });
108
+ },
109
+ );
110
+ }
111
+
112
+ async function clearPersistedEvents(): Promise<void> {
113
+ if (!options.store?.clear) return;
114
+
115
+ try {
116
+ await options.store.clear();
117
+ } catch (error) {
118
+ storeInternalError({
119
+ type: "error",
120
+ message: "Devtools persistence clear failed",
121
+ details: {
122
+ message: getErrorMessage(error),
123
+ store: options.store?.name,
124
+ },
125
+ });
126
+ }
127
+ }
128
+
129
+ function store(
130
+ event: DevtoolsEvent,
131
+ storeOptions: { notify?: boolean; persist?: boolean } = {},
132
+ ): DevtoolsEvent {
133
+ events.push(event);
134
+ if (events.length > maxEvents) {
135
+ events.splice(0, events.length - maxEvents);
136
+ }
137
+ if (storeOptions.notify !== false) {
138
+ notify({ type: "record", event });
139
+ }
140
+ if (storeOptions.persist !== false) {
141
+ persistEvent(event);
142
+ }
143
+ return event;
144
+ }
145
+
146
+ function redact(event: DevtoolsEvent): DevtoolsEvent {
147
+ try {
148
+ return applyDevtoolsRedaction(event, options.redact);
149
+ } catch (error) {
150
+ return createRedactionFailureEvent(error);
151
+ }
152
+ }
153
+
154
+ function storeInternalError(event: DevtoolsEventInput): DevtoolsEvent {
155
+ const normalized = createDevtoolsEvent(event);
156
+ const redacted = redact(normalized);
157
+ if (!isDevtoolsEventEnabled(watchers, redacted)) return redacted;
158
+ return store(redacted, { persist: false });
159
+ }
160
+
161
+ for (const event of options.initialEvents ?? []) {
162
+ const redacted = redact(event);
163
+ if (isDevtoolsEventEnabled(watchers, redacted)) {
164
+ store(redacted, { notify: false, persist: false });
165
+ }
166
+ }
167
+
168
+ function log(event: DevtoolsEvent): void {
169
+ const redacted = redact(event);
170
+ if (!isDevtoolsEventEnabled(watchers, redacted)) return;
171
+ store(redacted);
172
+ }
173
+
174
+ function record(event: DevtoolsEventInput): DevtoolsEvent {
175
+ const normalized = createDevtoolsEvent(event);
176
+ const redacted = redact(normalized);
177
+ if (!isDevtoolsEventEnabled(watchers, redacted)) return redacted;
178
+ return store(redacted);
179
+ }
180
+
181
+ function getEvents(filter?: DevtoolsFilter): DevtoolsEvent[] {
182
+ let result = [...events];
183
+
184
+ if (filter?.type) {
185
+ result = result.filter((e) => e.type === filter.type);
186
+ }
187
+ if (filter?.requestId) {
188
+ result = result.filter((e) => e.requestId === filter.requestId);
189
+ }
190
+ if (filter?.traceId) {
191
+ result = result.filter((e) => e.traceId === filter.traceId);
192
+ }
193
+
194
+ const limit = filter?.limit ?? 200;
195
+ if (result.length > limit) {
196
+ result = result.slice(result.length - limit);
197
+ }
198
+
199
+ return result;
200
+ }
201
+
202
+ async function clear(): Promise<void> {
203
+ events.length = 0;
204
+ notify({ type: "clear" });
205
+ await clearPersistedEvents();
206
+ }
207
+
208
+ function getWatchers(): DevtoolsWatcher[] {
209
+ return watchers.map((watcher) => ({ ...watcher }));
210
+ }
211
+
212
+ function isWatcherEnabled(name: string): boolean {
213
+ return isDevtoolsWatcherEnabled(watchers, name);
214
+ }
215
+
216
+ function subscribe(listener: DevtoolsListener): () => void {
217
+ listeners.add(listener);
218
+ return () => {
219
+ listeners.delete(listener);
220
+ };
221
+ }
222
+
223
+ return {
224
+ log,
225
+ record,
226
+ subscribe,
227
+ getEvents,
228
+ clear,
229
+ getWatchers,
230
+ isWatcherEnabled,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Configuration schema for the devtools provider.
236
+ */
237
+ const DevtoolsConfigSchema = z.object({
238
+ ENABLED: z
239
+ .string()
240
+ .optional()
241
+ .transform((v) => (v === undefined ? undefined : v === "true")),
242
+ MAX_EVENTS: z.coerce.number().int().positive().optional(),
243
+ PERSIST: z
244
+ .string()
245
+ .optional()
246
+ .transform((v) => (v === undefined ? undefined : v === "true")),
247
+ PERSIST_PATH: z.string().optional(),
248
+ });
249
+
250
+ export type DevtoolsConfig = z.infer<typeof DevtoolsConfigSchema>;
251
+
252
+ export interface DevtoolsProviderOptions {
253
+ enabled?: boolean;
254
+ maxEvents?: number;
255
+ redact?: DevtoolsRedactor;
256
+ watchers?: DevtoolsWatchersOptions;
257
+ store?:
258
+ | DevtoolsEventStore
259
+ | (() => DevtoolsEventStore | Promise<DevtoolsEventStore>);
260
+ }
261
+
262
+ /**
263
+ * Devtools service provider.
264
+ *
265
+ * This provider attaches a DevtoolsPort to ctx.ports.devtools.
266
+ * It can be enabled/disabled via the DEVTOOLS_ENABLED environment variable.
267
+ * By default, it's enabled in non-production environments and disabled in production.
268
+ *
269
+ * @example
270
+ * ```ts
271
+ * import { createDevtoolsProvider } from "@beignet/devtools";
272
+ * import { createServer } from "@beignet/core/server";
273
+ *
274
+ * const server = await createServer({
275
+ * ports,
276
+ * providers: [createDevtoolsProvider()],
277
+ * });
278
+ * ```
279
+ *
280
+ * To explicitly enable/disable:
281
+ * ```bash
282
+ * DEVTOOLS_ENABLED=true npm run dev
283
+ * DEVTOOLS_ENABLED=false npm run dev
284
+ * ```
285
+ */
286
+ export function createDevtoolsProvider(options: DevtoolsProviderOptions = {}) {
287
+ return createProvider<
288
+ Record<string, unknown>,
289
+ typeof DevtoolsConfigSchema,
290
+ { devtools: DevtoolsPort }
291
+ >({
292
+ name: "devtools",
293
+
294
+ config: {
295
+ schema: DevtoolsConfigSchema,
296
+ envPrefix: "DEVTOOLS_",
297
+ },
298
+
299
+ async setup({ config }) {
300
+ const enabled =
301
+ options.enabled ??
302
+ config?.ENABLED ??
303
+ process.env.NODE_ENV !== "production";
304
+
305
+ if (!enabled) {
306
+ const watchers = resolveDevtoolsWatchers(options.watchers).map(
307
+ (watcher) => ({
308
+ ...watcher,
309
+ enabled: false,
310
+ }),
311
+ );
312
+
313
+ // Provide a no-op implementation so callers don't need null checks
314
+ const noop: DevtoolsPort = {
315
+ log: () => {},
316
+ record: (event) => createDevtoolsEvent(event),
317
+ subscribe: () => () => {},
318
+ getEvents: () => [],
319
+ clear: () => {},
320
+ getWatchers: () => watchers.map((watcher) => ({ ...watcher })),
321
+ isWatcherEnabled: () => false,
322
+ };
323
+ return { ports: { devtools: noop } };
324
+ }
325
+
326
+ const maxEvents = config?.MAX_EVENTS ?? options.maxEvents ?? MAX_EVENTS;
327
+ const configuredStore =
328
+ typeof options.store === "function"
329
+ ? await options.store()
330
+ : options.store;
331
+ let store =
332
+ configuredStore ??
333
+ (config?.PERSIST || config?.PERSIST_PATH
334
+ ? createFileDevtoolsStore({
335
+ filePath: config.PERSIST_PATH,
336
+ maxEvents,
337
+ })
338
+ : undefined);
339
+ let initialEvents: DevtoolsEvent[] = [];
340
+ let loadError: unknown;
341
+
342
+ if (store?.load) {
343
+ try {
344
+ initialEvents = await store.load();
345
+ } catch (error) {
346
+ loadError = error;
347
+ store = undefined;
348
+ }
349
+ }
350
+
351
+ const devtools = createInMemoryDevtools({
352
+ maxEvents,
353
+ redact: options.redact,
354
+ watchers: options.watchers,
355
+ initialEvents,
356
+ store,
357
+ });
358
+
359
+ if (loadError) {
360
+ devtools.record({
361
+ type: "error",
362
+ message: "Devtools persistence load failed",
363
+ details: {
364
+ message:
365
+ loadError instanceof Error
366
+ ? loadError.message
367
+ : String(loadError),
368
+ },
369
+ });
370
+ }
371
+
372
+ return { ports: { devtools } };
373
+ },
374
+ });
375
+ }
@@ -0,0 +1,26 @@
1
+ import { redactValue } from "@beignet/core/ports";
2
+ import type { DevtoolsEvent, DevtoolsRedactor } from "./events";
3
+
4
+ export const defaultDevtoolsRedactor: DevtoolsRedactor = (event) =>
5
+ redactValue(event);
6
+
7
+ export function applyDevtoolsRedaction(
8
+ event: DevtoolsEvent,
9
+ redact?: DevtoolsRedactor,
10
+ ): DevtoolsEvent {
11
+ const redacted = defaultDevtoolsRedactor(event);
12
+ if (!redact) return redacted;
13
+ return redact(redacted);
14
+ }
15
+
16
+ export function createRedactionFailureEvent(error: unknown): DevtoolsEvent {
17
+ return {
18
+ id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
19
+ timestamp: new Date().toISOString(),
20
+ type: "error",
21
+ message: "Devtools redactor failed",
22
+ details: {
23
+ message: error instanceof Error ? error.message : String(error),
24
+ },
25
+ };
26
+ }