@promptbook/cli 0.112.0-110 → 0.112.0-112

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 (44) hide show
  1. package/apps/agents-server/src/app/admin/servers/CreateServerDialog.tsx +16 -0
  2. package/apps/agents-server/src/app/admin/servers/useCreateServerWizard.ts +31 -5
  3. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +7 -5
  4. package/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/stream/route.ts +4 -86
  5. package/apps/agents-server/src/app/api/admin/servers/route.ts +5 -0
  6. package/apps/agents-server/src/app/api/metadata/route.ts +4 -0
  7. package/apps/agents-server/src/components/ApplicationErrorPage/ApplicationErrorPage.tsx +118 -12
  8. package/apps/agents-server/src/database/customJavascript.ts +62 -1
  9. package/apps/agents-server/src/database/customStylesheet.ts +60 -1
  10. package/apps/agents-server/src/database/getMetadata.ts +84 -3
  11. package/apps/agents-server/src/instrumentation.ts +3 -0
  12. package/apps/agents-server/src/utils/agentRouting/resolveAgentRouteTarget.ts +27 -62
  13. package/apps/agents-server/src/utils/errorReporting/applicationErrorHandling.ts +45 -0
  14. package/apps/agents-server/src/utils/errorReporting/refreshApplicationDocument.ts +10 -0
  15. package/apps/agents-server/src/utils/errorReporting/registerServerErrorSentryLogging.ts +331 -0
  16. package/apps/agents-server/src/utils/errorReporting/sendApplicationErrorReportToSentry.ts +8 -153
  17. package/apps/agents-server/src/utils/errorReporting/sentryStore.ts +177 -0
  18. package/apps/agents-server/src/utils/importAgent.ts +1 -57
  19. package/apps/agents-server/src/utils/importAgentWithFallback.ts +0 -10
  20. package/apps/agents-server/src/utils/serverManagement/createManagedServer/bootstrapManagedServer.ts +3 -1
  21. package/apps/agents-server/src/utils/serverManagement/createManagedServer/normalizeCreateServerInput.ts +6 -0
  22. package/apps/agents-server/src/utils/serverManagement/createManagedServer/seedServerDefaultAgents.ts +7 -3
  23. package/apps/agents-server/src/utils/serverManagement/createManagedServer.ts +5 -0
  24. package/apps/agents-server/src/utils/userChat/listUserChats.ts +109 -0
  25. package/apps/agents-server/src/utils/userChat.ts +0 -1
  26. package/esm/index.es.js +39 -13
  27. package/esm/index.es.js.map +1 -1
  28. package/esm/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +2 -1
  29. package/esm/src/cli/cli-commands/agents-server/startAgentsServer.test.d.ts +1 -0
  30. package/esm/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -0
  31. package/esm/src/version.d.ts +1 -1
  32. package/package.json +1 -1
  33. package/src/cli/cli-commands/agents-server/startAgentsServer.ts +23 -2
  34. package/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.ts +91 -14
  35. package/src/other/templates/getTemplatesPipelineCollection.ts +801 -652
  36. package/src/version.ts +2 -2
  37. package/src/versions.txt +2 -0
  38. package/umd/index.umd.js +39 -13
  39. package/umd/index.umd.js.map +1 -1
  40. package/umd/src/cli/cli-commands/agents-server/startAgentsServer.d.ts +2 -1
  41. package/umd/src/cli/cli-commands/agents-server/startAgentsServer.test.d.ts +1 -0
  42. package/umd/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -0
  43. package/umd/src/version.d.ts +1 -1
  44. package/apps/agents-server/src/utils/userChat/getUserChatRevision.ts +0 -145
@@ -0,0 +1,331 @@
1
+ import { inspect } from 'node:util';
2
+ import { DEFAULT_APPLICATION_ERROR_SERVER_NAME } from './applicationErrorHandling';
3
+ import {
4
+ createSentryTimestamp,
5
+ resolveOptionalSentryDsn,
6
+ sendSentryStorePayload,
7
+ type SentryStorePayload,
8
+ } from './sentryStore';
9
+
10
+ /**
11
+ * Logger name visible in Sentry events for server-side `console.error` calls.
12
+ */
13
+ const SENTRY_SERVER_ERROR_LOGGER = 'agents-server.server-error';
14
+
15
+ /**
16
+ * Marker used to keep the global console bridge idempotent across hot reloads.
17
+ */
18
+ const SERVER_ERROR_SENTRY_LOGGING_STATE_SYMBOL = Symbol.for('promptbook.agents-server.serverErrorSentryLoggingState');
19
+
20
+ /**
21
+ * Default label used when `console.error` is called without a string or error object.
22
+ */
23
+ const DEFAULT_SERVER_ERROR_MESSAGE = 'Agents Server console.error';
24
+
25
+ /**
26
+ * Keys that commonly hold nested error payloads inside structured logs.
27
+ */
28
+ const NESTED_ERROR_PROPERTY_NAMES = ['error', 'cause', 'reason'] as const;
29
+
30
+ /**
31
+ * Maximum traversal depth when looking for nested error-like objects.
32
+ */
33
+ const MAX_ERROR_SEARCH_DEPTH = 2;
34
+
35
+ /**
36
+ * Shared global state for the server-side console bridge.
37
+ */
38
+ type ServerErrorSentryLoggingState = {
39
+ /**
40
+ * Original console implementation preserved before patching.
41
+ */
42
+ originalConsoleError: typeof console.error | null;
43
+
44
+ /**
45
+ * True once the bridge has been installed.
46
+ */
47
+ isRegistered: boolean;
48
+ };
49
+
50
+ /**
51
+ * Normalized error details extracted from logged values.
52
+ */
53
+ type LoggedErrorInfo = {
54
+ /**
55
+ * Best available error type.
56
+ */
57
+ name: string;
58
+
59
+ /**
60
+ * Best available error message.
61
+ */
62
+ message: string;
63
+
64
+ /**
65
+ * Optional stack trace when present.
66
+ */
67
+ stack?: string;
68
+ };
69
+
70
+ /**
71
+ * Registers one server-side `console.error` bridge that forwards logs to Sentry.
72
+ */
73
+ export function registerServerErrorSentryLogging(): void {
74
+ const loggingState = getServerErrorSentryLoggingState();
75
+
76
+ if (loggingState.isRegistered) {
77
+ return;
78
+ }
79
+
80
+ loggingState.originalConsoleError = console.error.bind(console);
81
+ console.error = (...consoleArguments: unknown[]): void => {
82
+ loggingState.originalConsoleError?.(...consoleArguments);
83
+
84
+ const sentryDsn = resolveOptionalSentryDsn();
85
+ if (!sentryDsn) {
86
+ return;
87
+ }
88
+
89
+ const sentryPayload = createServerErrorSentryStorePayload(consoleArguments);
90
+
91
+ // Never log reporting failures through `console.error`, otherwise a broken Sentry configuration would recurse.
92
+ void sendSentryStorePayload(sentryPayload, sentryDsn).catch(() => undefined);
93
+ };
94
+
95
+ loggingState.isRegistered = true;
96
+ }
97
+
98
+ /**
99
+ * Restores the original `console.error` implementation after tests patch it.
100
+ *
101
+ * @private test helper for server-side Sentry console forwarding
102
+ */
103
+ export function $resetServerErrorSentryLoggingForTests(): void {
104
+ const loggingState = getServerErrorSentryLoggingState();
105
+
106
+ if (loggingState.originalConsoleError) {
107
+ console.error = loggingState.originalConsoleError;
108
+ }
109
+
110
+ loggingState.originalConsoleError = null;
111
+ loggingState.isRegistered = false;
112
+ }
113
+
114
+ /**
115
+ * Returns the shared mutable global state used by the bridge.
116
+ *
117
+ * @returns Shared state object reused across reloads.
118
+ */
119
+ function getServerErrorSentryLoggingState(): ServerErrorSentryLoggingState {
120
+ const globalObject = globalThis as typeof globalThis & Record<PropertyKey, unknown>;
121
+
122
+ const existingLoggingState = globalObject[
123
+ SERVER_ERROR_SENTRY_LOGGING_STATE_SYMBOL
124
+ ] as ServerErrorSentryLoggingState | undefined;
125
+ if (existingLoggingState) {
126
+ return existingLoggingState;
127
+ }
128
+
129
+ const newLoggingState: ServerErrorSentryLoggingState = {
130
+ originalConsoleError: null,
131
+ isRegistered: false,
132
+ };
133
+
134
+ globalObject[SERVER_ERROR_SENTRY_LOGGING_STATE_SYMBOL] = newLoggingState;
135
+ return newLoggingState;
136
+ }
137
+
138
+ /**
139
+ * Creates one Sentry store payload from a `console.error` call.
140
+ *
141
+ * @param consoleArguments - Original `console.error` arguments.
142
+ * @returns Structured Sentry event payload.
143
+ */
144
+ function createServerErrorSentryStorePayload(consoleArguments: readonly unknown[]): SentryStorePayload {
145
+ const loggedError = findLoggedErrorInfo(consoleArguments);
146
+
147
+ return {
148
+ platform: 'javascript',
149
+ level: 'error',
150
+ logger: SENTRY_SERVER_ERROR_LOGGER,
151
+ timestamp: createSentryTimestamp(),
152
+ message: createServerErrorMessage(consoleArguments, loggedError),
153
+ server_name: process.env.NEXT_PUBLIC_SERVER_NAME ?? DEFAULT_APPLICATION_ERROR_SERVER_NAME,
154
+ tags: {
155
+ source: 'agents-server.console-error',
156
+ nextRuntime: process.env.NEXT_RUNTIME ?? 'nodejs',
157
+ nodeEnv: process.env.NODE_ENV ?? 'unknown',
158
+ },
159
+ exception: loggedError
160
+ ? {
161
+ values: [
162
+ {
163
+ type: loggedError.name,
164
+ value: loggedError.message,
165
+ },
166
+ ],
167
+ }
168
+ : undefined,
169
+ extra: {
170
+ consoleArguments: consoleArguments.map(serializeConsoleArgument),
171
+ errorStack: loggedError?.stack ?? null,
172
+ vercelEnv: process.env.VERCEL_ENV ?? null,
173
+ vercelRegion: process.env.VERCEL_REGION ?? null,
174
+ vercelUrl: process.env.VERCEL_URL ?? null,
175
+ },
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Creates one stable event message from a `console.error` call.
181
+ *
182
+ * @param consoleArguments - Original `console.error` arguments.
183
+ * @param loggedError - First extracted error-like payload when available.
184
+ * @returns Event message suitable for Sentry grouping and scanning.
185
+ */
186
+ function createServerErrorMessage(
187
+ consoleArguments: readonly unknown[],
188
+ loggedError: LoggedErrorInfo | null,
189
+ ): string {
190
+ const stringMessage = consoleArguments
191
+ .filter((consoleArgument): consoleArgument is string => typeof consoleArgument === 'string')
192
+ .map((consoleArgument) => consoleArgument.trim())
193
+ .filter(Boolean)
194
+ .join(' ')
195
+ .trim();
196
+
197
+ if (stringMessage && loggedError?.message && !stringMessage.includes(loggedError.message)) {
198
+ return `${stringMessage} ${loggedError.message}`.trim();
199
+ }
200
+
201
+ if (stringMessage) {
202
+ return stringMessage;
203
+ }
204
+
205
+ if (loggedError?.message) {
206
+ return loggedError.message;
207
+ }
208
+
209
+ const firstConsoleArgument = consoleArguments.at(0);
210
+ if (firstConsoleArgument !== undefined) {
211
+ return serializeConsoleArgument(firstConsoleArgument);
212
+ }
213
+
214
+ return DEFAULT_SERVER_ERROR_MESSAGE;
215
+ }
216
+
217
+ /**
218
+ * Extracts the first meaningful error-like payload from structured console arguments.
219
+ *
220
+ * @param consoleArguments - Original `console.error` arguments.
221
+ * @returns Normalized logged error payload or `null`.
222
+ */
223
+ function findLoggedErrorInfo(consoleArguments: readonly unknown[]): LoggedErrorInfo | null {
224
+ const visitedObjects = new WeakSet<object>();
225
+
226
+ for (const consoleArgument of consoleArguments) {
227
+ const loggedError = findLoggedErrorInfoInValue(consoleArgument, visitedObjects, 0);
228
+ if (loggedError) {
229
+ return loggedError;
230
+ }
231
+ }
232
+
233
+ return null;
234
+ }
235
+
236
+ /**
237
+ * Recursively searches one logged value for error-like payloads.
238
+ *
239
+ * @param value - Current logged value.
240
+ * @param visitedObjects - Cycle guard for nested objects.
241
+ * @param depth - Current recursion depth.
242
+ * @returns Normalized logged error payload or `null`.
243
+ */
244
+ function findLoggedErrorInfoInValue(
245
+ value: unknown,
246
+ visitedObjects: WeakSet<object>,
247
+ depth: number,
248
+ ): LoggedErrorInfo | null {
249
+ if (value instanceof Error) {
250
+ return {
251
+ name: value.name || 'Error',
252
+ message: value.message || DEFAULT_SERVER_ERROR_MESSAGE,
253
+ stack: value.stack,
254
+ };
255
+ }
256
+
257
+ if (!value || typeof value !== 'object') {
258
+ return null;
259
+ }
260
+
261
+ if (visitedObjects.has(value)) {
262
+ return null;
263
+ }
264
+ visitedObjects.add(value);
265
+
266
+ const normalizedErrorInfo = normalizeLoggedErrorInfo(value as Record<string, unknown>);
267
+ if (normalizedErrorInfo) {
268
+ return normalizedErrorInfo;
269
+ }
270
+
271
+ if (depth >= MAX_ERROR_SEARCH_DEPTH) {
272
+ return null;
273
+ }
274
+
275
+ for (const propertyName of NESTED_ERROR_PROPERTY_NAMES) {
276
+ if (!(propertyName in value)) {
277
+ continue;
278
+ }
279
+
280
+ const nestedErrorInfo = findLoggedErrorInfoInValue(
281
+ (value as Record<string, unknown>)[propertyName],
282
+ visitedObjects,
283
+ depth + 1,
284
+ );
285
+ if (nestedErrorInfo) {
286
+ return nestedErrorInfo;
287
+ }
288
+ }
289
+
290
+ return null;
291
+ }
292
+
293
+ /**
294
+ * Converts one plain object into normalized error details when it looks like an error.
295
+ *
296
+ * @param value - Plain logged object.
297
+ * @returns Normalized logged error payload or `null`.
298
+ */
299
+ function normalizeLoggedErrorInfo(value: Record<string, unknown>): LoggedErrorInfo | null {
300
+ const rawErrorName = typeof value.name === 'string' ? value.name.trim() : '';
301
+ const rawErrorMessage = typeof value.message === 'string' ? value.message.trim() : '';
302
+ const rawErrorStack = typeof value.stack === 'string' ? value.stack.trim() : '';
303
+
304
+ if (!rawErrorName && !rawErrorStack) {
305
+ return null;
306
+ }
307
+
308
+ return {
309
+ name: rawErrorName || 'Error',
310
+ message: rawErrorMessage || DEFAULT_SERVER_ERROR_MESSAGE,
311
+ stack: rawErrorStack || undefined,
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Serializes one logged console value into a stable string representation.
317
+ *
318
+ * @param value - Original console argument.
319
+ * @returns Stable string suitable for Sentry `extra`.
320
+ */
321
+ function serializeConsoleArgument(value: unknown): string {
322
+ if (typeof value === 'string') {
323
+ return value;
324
+ }
325
+
326
+ return inspect(value, {
327
+ depth: 6,
328
+ breakLength: 120,
329
+ maxArrayLength: 50,
330
+ });
331
+ }
@@ -1,146 +1,16 @@
1
1
  import type { ApplicationErrorReportPayload } from './applicationErrorHandling';
2
-
3
- /**
4
- * Sentry protocol version used for store endpoint requests.
5
- */
6
- const SENTRY_PROTOCOL_VERSION = '7';
7
-
8
- /**
9
- * Endpoint content type used when sending JSON payloads to Sentry.
10
- */
11
- const JSON_CONTENT_TYPE = 'application/json';
2
+ import {
3
+ createSentryTimestamp,
4
+ resolveRequiredSentryDsn,
5
+ sendSentryStorePayload,
6
+ type SentryStorePayload,
7
+ } from './sentryStore';
12
8
 
13
9
  /**
14
10
  * Logger name visible in Sentry events.
15
11
  */
16
12
  const SENTRY_APPLICATION_ERROR_LOGGER = 'agents-server.application-error';
17
13
 
18
- /**
19
- * Number of milliseconds in one second.
20
- */
21
- const MILLISECONDS_IN_SECOND = 1000;
22
-
23
- /**
24
- * Minimal Sentry DSN parts needed for store API requests.
25
- */
26
- type SentryDsnParts = {
27
- /**
28
- * Store endpoint derived from DSN host/project.
29
- */
30
- storeEndpoint: URL;
31
- };
32
-
33
- /**
34
- * Payload shape submitted to the Sentry store endpoint.
35
- */
36
- type SentryStorePayload = {
37
- /**
38
- * Event platform.
39
- */
40
- platform: 'javascript';
41
-
42
- /**
43
- * Event level.
44
- */
45
- level: 'error';
46
-
47
- /**
48
- * Event logger value.
49
- */
50
- logger: string;
51
-
52
- /**
53
- * Seconds since UNIX epoch.
54
- */
55
- timestamp: number;
56
-
57
- /**
58
- * Human readable message.
59
- */
60
- message: string;
61
-
62
- /**
63
- * Server/deployment name.
64
- */
65
- server_name: string;
66
-
67
- /**
68
- * Diagnostic tags used for filtering.
69
- */
70
- tags: Record<string, string>;
71
-
72
- /**
73
- * Structured exception details.
74
- */
75
- exception: {
76
- /**
77
- * Individual exception list.
78
- */
79
- values: Array<{
80
- /**
81
- * Exception type.
82
- */
83
- type: string;
84
-
85
- /**
86
- * Exception message.
87
- */
88
- value: string;
89
- }>;
90
- };
91
-
92
- /**
93
- * Additional diagnostic payload.
94
- */
95
- extra: Record<string, string | null>;
96
- };
97
-
98
- /**
99
- * Resolves Sentry DSN from environment.
100
- *
101
- * @returns Raw DSN string.
102
- * @throws Error when DSN is missing.
103
- */
104
- function resolveSentryDsn(): string {
105
- const dsn = process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN;
106
- if (!dsn) {
107
- throw new Error('Missing Sentry DSN. Configure SENTRY_DSN or NEXT_PUBLIC_SENTRY_DSN.');
108
- }
109
-
110
- return dsn;
111
- }
112
-
113
- /**
114
- * Parses a DSN into pieces required for Sentry store API requests.
115
- *
116
- * @param dsn - Raw Sentry DSN.
117
- * @returns Parsed DSN details.
118
- * @throws Error when DSN format is invalid.
119
- */
120
- function parseSentryDsn(dsn: string): SentryDsnParts {
121
- const dsnUrl = new URL(dsn);
122
- const pathSegments = dsnUrl.pathname.split('/').filter(Boolean);
123
- const projectId = pathSegments.at(-1);
124
- const pathPrefix = pathSegments.slice(0, -1).join('/');
125
-
126
- if (!projectId) {
127
- throw new Error('Invalid Sentry DSN: missing project ID.');
128
- }
129
-
130
- if (!dsnUrl.username) {
131
- throw new Error('Invalid Sentry DSN: missing public key.');
132
- }
133
-
134
- const basePath = pathPrefix ? `/${pathPrefix}` : '';
135
- const storeEndpoint = new URL(`${dsnUrl.protocol}//${dsnUrl.host}${basePath}/api/${projectId}/store/`);
136
- storeEndpoint.searchParams.set('sentry_key', dsnUrl.username);
137
- storeEndpoint.searchParams.set('sentry_version', SENTRY_PROTOCOL_VERSION);
138
-
139
- return {
140
- storeEndpoint,
141
- };
142
- }
143
-
144
14
  /**
145
15
  * Creates the Sentry-compatible JSON payload from the application report.
146
16
  *
@@ -152,7 +22,7 @@ function createSentryStorePayload(report: ApplicationErrorReportPayload): Sentry
152
22
  platform: 'javascript',
153
23
  level: 'error',
154
24
  logger: SENTRY_APPLICATION_ERROR_LOGGER,
155
- timestamp: Date.now() / MILLISECONDS_IN_SECOND,
25
+ timestamp: createSentryTimestamp(),
156
26
  message: report.errorMessage,
157
27
  server_name: report.serverName,
158
28
  tags: {
@@ -183,20 +53,5 @@ function createSentryStorePayload(report: ApplicationErrorReportPayload): Sentry
183
53
  * @param report - Structured browser report payload.
184
54
  */
185
55
  export async function sendApplicationErrorReportToSentry(report: ApplicationErrorReportPayload): Promise<void> {
186
- const dsn = resolveSentryDsn();
187
- const { storeEndpoint } = parseSentryDsn(dsn);
188
- const payload = createSentryStorePayload(report);
189
-
190
- const response = await fetch(storeEndpoint, {
191
- method: 'POST',
192
- headers: {
193
- 'Content-Type': JSON_CONTENT_TYPE,
194
- },
195
- body: JSON.stringify(payload),
196
- });
197
-
198
- if (!response.ok) {
199
- const responseBody = await response.text();
200
- throw new Error(`Sentry rejected application error report (${response.status}): ${responseBody}`);
201
- }
56
+ await sendSentryStorePayload(createSentryStorePayload(report), resolveRequiredSentryDsn());
202
57
  }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Sentry protocol version used for store endpoint requests.
3
+ */
4
+ const SENTRY_PROTOCOL_VERSION = '7';
5
+
6
+ /**
7
+ * Endpoint content type used when sending JSON payloads to Sentry.
8
+ */
9
+ const JSON_CONTENT_TYPE = 'application/json';
10
+
11
+ /**
12
+ * Number of milliseconds in one second.
13
+ */
14
+ const MILLISECONDS_IN_SECOND = 1000;
15
+
16
+ /**
17
+ * Minimal Sentry DSN parts needed for store endpoint requests.
18
+ */
19
+ type SentryDsnParts = {
20
+ /**
21
+ * Store endpoint derived from DSN host/project.
22
+ */
23
+ storeEndpoint: URL;
24
+ };
25
+
26
+ /**
27
+ * Payload shape submitted to the Sentry store endpoint.
28
+ */
29
+ export type SentryStorePayload = {
30
+ /**
31
+ * Event platform.
32
+ */
33
+ platform: 'javascript';
34
+
35
+ /**
36
+ * Event level.
37
+ */
38
+ level: 'error';
39
+
40
+ /**
41
+ * Logger name visible in Sentry events.
42
+ */
43
+ logger: string;
44
+
45
+ /**
46
+ * Seconds since UNIX epoch.
47
+ */
48
+ timestamp: number;
49
+
50
+ /**
51
+ * Human readable message.
52
+ */
53
+ message: string;
54
+
55
+ /**
56
+ * Server/deployment name.
57
+ */
58
+ server_name: string;
59
+
60
+ /**
61
+ * Optional diagnostic tags used for filtering.
62
+ */
63
+ tags?: Record<string, string>;
64
+
65
+ /**
66
+ * Optional structured exception details.
67
+ */
68
+ exception?: {
69
+ /**
70
+ * Individual exception list.
71
+ */
72
+ values: Array<{
73
+ /**
74
+ * Exception type.
75
+ */
76
+ type: string;
77
+
78
+ /**
79
+ * Exception message.
80
+ */
81
+ value: string;
82
+ }>;
83
+ };
84
+
85
+ /**
86
+ * Optional additional diagnostic payload.
87
+ */
88
+ extra?: Record<string, unknown>;
89
+ };
90
+
91
+ /**
92
+ * Resolves Sentry DSN from environment when configured.
93
+ *
94
+ * @returns Raw DSN string or `null` when telemetry is not configured.
95
+ */
96
+ export function resolveOptionalSentryDsn(): string | null {
97
+ return process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN ?? null;
98
+ }
99
+
100
+ /**
101
+ * Resolves Sentry DSN from environment.
102
+ *
103
+ * @returns Raw DSN string.
104
+ * @throws Error when DSN is missing.
105
+ */
106
+ export function resolveRequiredSentryDsn(): string {
107
+ const sentryDsn = resolveOptionalSentryDsn();
108
+
109
+ if (!sentryDsn) {
110
+ throw new Error('Missing Sentry DSN. Configure SENTRY_DSN or NEXT_PUBLIC_SENTRY_DSN.');
111
+ }
112
+
113
+ return sentryDsn;
114
+ }
115
+
116
+ /**
117
+ * Parses a DSN into pieces required for Sentry store API requests.
118
+ *
119
+ * @param sentryDsn - Raw Sentry DSN.
120
+ * @returns Parsed DSN details.
121
+ * @throws Error when DSN format is invalid.
122
+ */
123
+ function parseSentryDsn(sentryDsn: string): SentryDsnParts {
124
+ const sentryDsnUrl = new URL(sentryDsn);
125
+ const pathSegments = sentryDsnUrl.pathname.split('/').filter(Boolean);
126
+ const projectId = pathSegments.at(-1);
127
+ const pathPrefix = pathSegments.slice(0, -1).join('/');
128
+
129
+ if (!projectId) {
130
+ throw new Error('Invalid Sentry DSN: missing project ID.');
131
+ }
132
+
133
+ if (!sentryDsnUrl.username) {
134
+ throw new Error('Invalid Sentry DSN: missing public key.');
135
+ }
136
+
137
+ const basePath = pathPrefix ? `/${pathPrefix}` : '';
138
+ const storeEndpoint = new URL(`${sentryDsnUrl.protocol}//${sentryDsnUrl.host}${basePath}/api/${projectId}/store/`);
139
+ storeEndpoint.searchParams.set('sentry_key', sentryDsnUrl.username);
140
+ storeEndpoint.searchParams.set('sentry_version', SENTRY_PROTOCOL_VERSION);
141
+
142
+ return {
143
+ storeEndpoint,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Creates a current Sentry timestamp expressed in seconds.
149
+ *
150
+ * @returns Current event timestamp in seconds.
151
+ */
152
+ export function createSentryTimestamp(): number {
153
+ return Date.now() / MILLISECONDS_IN_SECOND;
154
+ }
155
+
156
+ /**
157
+ * Sends one Sentry store payload.
158
+ *
159
+ * @param payload - Fully constructed Sentry store payload.
160
+ * @param sentryDsn - Raw Sentry DSN used to derive the store endpoint.
161
+ */
162
+ export async function sendSentryStorePayload(payload: SentryStorePayload, sentryDsn: string): Promise<void> {
163
+ const { storeEndpoint } = parseSentryDsn(sentryDsn);
164
+
165
+ const response = await fetch(storeEndpoint, {
166
+ method: 'POST',
167
+ headers: {
168
+ 'Content-Type': JSON_CONTENT_TYPE,
169
+ },
170
+ body: JSON.stringify(payload),
171
+ });
172
+
173
+ if (!response.ok) {
174
+ const responseBody = await response.text();
175
+ throw new Error(`Sentry rejected error report (${response.status}): ${responseBody}`);
176
+ }
177
+ }