@od-oneapp/observability 2026.1.1301

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 (107) hide show
  1. package/README.md +523 -0
  2. package/dist/client-next.d.mts +20 -0
  3. package/dist/client-next.d.mts.map +1 -0
  4. package/dist/client-next.mjs +64 -0
  5. package/dist/client-next.mjs.map +1 -0
  6. package/dist/client.d.mts +11 -0
  7. package/dist/client.d.mts.map +1 -0
  8. package/dist/client.mjs +47 -0
  9. package/dist/client.mjs.map +1 -0
  10. package/dist/env.d.mts +15 -0
  11. package/dist/env.d.mts.map +1 -0
  12. package/dist/env.mjs +45 -0
  13. package/dist/env.mjs.map +1 -0
  14. package/dist/factory-DkY353r8.mjs +380 -0
  15. package/dist/factory-DkY353r8.mjs.map +1 -0
  16. package/dist/hooks-useObservability.d.mts +11 -0
  17. package/dist/hooks-useObservability.d.mts.map +1 -0
  18. package/dist/hooks-useObservability.mjs +174 -0
  19. package/dist/hooks-useObservability.mjs.map +1 -0
  20. package/dist/index-CpcdzWrF.d.mts +24 -0
  21. package/dist/index-CpcdzWrF.d.mts.map +1 -0
  22. package/dist/index.d.mts +88 -0
  23. package/dist/index.d.mts.map +1 -0
  24. package/dist/index.mjs +97 -0
  25. package/dist/index.mjs.map +1 -0
  26. package/dist/manager-BxQqOPEg.d.mts +33 -0
  27. package/dist/manager-BxQqOPEg.d.mts.map +1 -0
  28. package/dist/plugin-Bfq-o3nr.d.mts +60 -0
  29. package/dist/plugin-Bfq-o3nr.d.mts.map +1 -0
  30. package/dist/plugin-Bt-ygG1m.d.mts +254 -0
  31. package/dist/plugin-Bt-ygG1m.d.mts.map +1 -0
  32. package/dist/plugin-CLFwRERa.mjs +593 -0
  33. package/dist/plugin-CLFwRERa.mjs.map +1 -0
  34. package/dist/plugin-CP895lBx.mjs +534 -0
  35. package/dist/plugin-CP895lBx.mjs.map +1 -0
  36. package/dist/plugin-CaQxviDs.d.mts +61 -0
  37. package/dist/plugin-CaQxviDs.d.mts.map +1 -0
  38. package/dist/plugin-lPdJirTY.mjs +234 -0
  39. package/dist/plugin-lPdJirTY.mjs.map +1 -0
  40. package/dist/plugins-betterstack-env.d.mts +29 -0
  41. package/dist/plugins-betterstack-env.d.mts.map +1 -0
  42. package/dist/plugins-betterstack-env.mjs +75 -0
  43. package/dist/plugins-betterstack-env.mjs.map +1 -0
  44. package/dist/plugins-betterstack.d.mts +4 -0
  45. package/dist/plugins-betterstack.mjs +4 -0
  46. package/dist/plugins-console.d.mts +37 -0
  47. package/dist/plugins-console.d.mts.map +1 -0
  48. package/dist/plugins-console.mjs +196 -0
  49. package/dist/plugins-console.mjs.map +1 -0
  50. package/dist/plugins-sentry-env.d.mts +37 -0
  51. package/dist/plugins-sentry-env.d.mts.map +1 -0
  52. package/dist/plugins-sentry-env.mjs +79 -0
  53. package/dist/plugins-sentry-env.mjs.map +1 -0
  54. package/dist/plugins-sentry-microfrontend-env.d.mts +49 -0
  55. package/dist/plugins-sentry-microfrontend-env.d.mts.map +1 -0
  56. package/dist/plugins-sentry-microfrontend-env.mjs +80 -0
  57. package/dist/plugins-sentry-microfrontend-env.mjs.map +1 -0
  58. package/dist/plugins-sentry-microfrontend.d.mts +2 -0
  59. package/dist/plugins-sentry-microfrontend.mjs +3 -0
  60. package/dist/plugins-sentry.d.mts +5 -0
  61. package/dist/plugins-sentry.mjs +6 -0
  62. package/dist/server-edge.d.mts +15 -0
  63. package/dist/server-edge.d.mts.map +1 -0
  64. package/dist/server-edge.mjs +53 -0
  65. package/dist/server-edge.mjs.map +1 -0
  66. package/dist/server-next.d.mts +17 -0
  67. package/dist/server-next.d.mts.map +1 -0
  68. package/dist/server-next.mjs +64 -0
  69. package/dist/server-next.mjs.map +1 -0
  70. package/dist/server.d.mts +11 -0
  71. package/dist/server.d.mts.map +1 -0
  72. package/dist/server.mjs +48 -0
  73. package/dist/server.mjs.map +1 -0
  74. package/dist/utils-CuGrTcD6.d.mts +77 -0
  75. package/dist/utils-CuGrTcD6.d.mts.map +1 -0
  76. package/env.ts +67 -0
  77. package/package.json +147 -0
  78. package/src/client-next.ts +131 -0
  79. package/src/client.ts +70 -0
  80. package/src/core/index.ts +15 -0
  81. package/src/core/manager.ts +361 -0
  82. package/src/core/plugin.ts +61 -0
  83. package/src/core/types.ts +151 -0
  84. package/src/factory/builder.ts +132 -0
  85. package/src/factory/index.ts +67 -0
  86. package/src/factory/presets.ts +78 -0
  87. package/src/hooks/useObservability.ts +206 -0
  88. package/src/plugins/betterstack/env.ts +101 -0
  89. package/src/plugins/betterstack/index.ts +15 -0
  90. package/src/plugins/betterstack/plugin.ts +373 -0
  91. package/src/plugins/console/index.ts +323 -0
  92. package/src/plugins/sentry/__tests__/plugin-tracing.test.ts +511 -0
  93. package/src/plugins/sentry/env.ts +93 -0
  94. package/src/plugins/sentry/index.ts +28 -0
  95. package/src/plugins/sentry/plugin.ts +953 -0
  96. package/src/plugins/sentry/types.ts +252 -0
  97. package/src/plugins/sentry-microfrontend/env.ts +105 -0
  98. package/src/plugins/sentry-microfrontend/index.ts +12 -0
  99. package/src/plugins/sentry-microfrontend/multiplexed-transport.ts +221 -0
  100. package/src/plugins/sentry-microfrontend/plugin.ts +500 -0
  101. package/src/plugins/sentry-microfrontend/sentry-types.ts +140 -0
  102. package/src/plugins/sentry-microfrontend/types.ts +130 -0
  103. package/src/plugins/sentry-microfrontend/utils.ts +326 -0
  104. package/src/server-edge.ts +113 -0
  105. package/src/server-next.ts +114 -0
  106. package/src/server.ts +71 -0
  107. package/src/shared.ts +148 -0
@@ -0,0 +1,953 @@
1
+ /**
2
+ * @fileoverview Sentry plugin implementation
3
+ * Sentry plugin implementation
4
+ */
5
+
6
+ import { logDebug, logError, logWarn } from '@repo/shared/logger';
7
+ import type { ObservabilityServerPlugin } from '../../core/plugin';
8
+ import type {
9
+ Breadcrumb,
10
+ LogLevel,
11
+ ObservabilityContext,
12
+ ObservabilityUser,
13
+ } from '../../core/types';
14
+ import { isBrowser } from '../../shared';
15
+ import { safeEnv } from './env';
16
+ import type { Hub, Scope, Span, SpanContext, Transaction, TransactionContext } from './types';
17
+
18
+ /**
19
+ * Default log filter function that filters out noisy development logs.
20
+ * Filters out:
21
+ * - Next.js Fast Refresh messages
22
+ * - HMR (Hot Module Replacement) messages
23
+ * - Prisma query logs (server-only)
24
+ * - Prisma engine/info logs
25
+ * - SQL queries matching Prisma's format
26
+ *
27
+ * Handles ANSI color codes, console formatting (%c, %s), and CSS styling.
28
+ *
29
+ * @param log - The log entry from Sentry
30
+ * @param isServer - Whether this is running on the server (defaults to true)
31
+ * @returns The log entry if it should be sent, or null to filter it out
32
+ */
33
+ export function defaultBeforeSendLog(log: any, isServer = true): any | null {
34
+ // Check both message and any formatted message fields
35
+ const messageToCheck = log.message || log.formatted || '';
36
+ if (!messageToCheck) {
37
+ return log; // No message to filter, send as-is
38
+ }
39
+
40
+ const message = String(messageToCheck);
41
+
42
+ // Remove ANSI color codes for comparison (e.g., \u001b[34mprisma:query\u001b[39m)
43
+ // Also handle %c format codes used by console styling and %s placeholders
44
+ const messageWithoutAnsi = message
45
+ .replace(/\u001b\[[0-9;]*m/g, '') // Remove ANSI escape codes
46
+ .replace(/%[csdfiO]/g, '') // Remove console format placeholders (%c, %s, %d, %f, %i, %o)
47
+ .replace(/background:[^;]+;?/g, '') // Remove CSS background styles
48
+ .replace(/color:[^;]+;?/g, '') // Remove CSS color styles
49
+ .replace(/border-radius:[^;]+;?/g, '') // Remove CSS border-radius
50
+ .replace(/light-dark\([^)]+\)/g, '') // Remove light-dark() CSS functions
51
+ .replace(/rgba?\([^)]+\)/g, '') // Remove rgba/rgb color functions
52
+ .replace(/#[0-9a-fA-F]{3,8}/g, '') // Remove hex colors
53
+ .toLowerCase(); // Case-insensitive matching
54
+
55
+ // Filter out Next.js Fast Refresh and HMR messages (both client and server)
56
+ if (messageWithoutAnsi.includes('[fast refresh]') || messageWithoutAnsi.includes('[hmr]')) {
57
+ return null; // Drop the log
58
+ }
59
+
60
+ // Filter out Prisma logs (server-only)
61
+ if (isServer) {
62
+ if (
63
+ messageWithoutAnsi.includes('prisma:query') ||
64
+ messageWithoutAnsi.includes('prisma:engine') ||
65
+ messageWithoutAnsi.includes('prisma:info') ||
66
+ // Also filter SQL queries that look like Prisma queries (SELECT ... FROM "public".)
67
+ (messageWithoutAnsi.includes('select') &&
68
+ messageWithoutAnsi.includes('from') &&
69
+ (messageWithoutAnsi.includes('"public".') || messageWithoutAnsi.includes('public.')))
70
+ ) {
71
+ return null; // Drop the log
72
+ }
73
+ }
74
+
75
+ return log; // Send the log
76
+ }
77
+
78
+ /**
79
+ * Minimal Sentry interface for common methods across all Sentry packages
80
+ */
81
+ interface SentryClient {
82
+ init(options: any): void;
83
+ captureException(error: any, captureContext?: any): string;
84
+ captureMessage(message: string, captureContext?: any): string;
85
+ setUser(user: any): void;
86
+ addBreadcrumb(breadcrumb: any): void;
87
+ withScope(callback: (scope: any) => void): void;
88
+ getCurrentScope?(): any;
89
+ getActiveTransaction?(): any;
90
+ startTransaction?(context: TransactionContext, customSamplingContext?: any): any;
91
+ startSpan?(context: SpanContext): any;
92
+ configureScope?(callback: (scope: any) => void): void;
93
+ getCurrentHub?(): any;
94
+ flush?(timeout?: number): Promise<boolean>;
95
+ close?(timeout?: number): Promise<boolean>;
96
+ browserTracingIntegration?(): any;
97
+ replayIntegration?(): any;
98
+ profilesIntegration?(): any;
99
+ consoleLoggingIntegration?(options?: { levels?: string[] }): any;
100
+ logger?: {
101
+ trace(message: string, attributes?: Record<string, unknown>): void;
102
+ debug(message: string, attributes?: Record<string, unknown>): void;
103
+ info(message: string, attributes?: Record<string, unknown>): void;
104
+ warn(message: string, attributes?: Record<string, unknown>): void;
105
+ error(message: string, attributes?: Record<string, unknown>): void;
106
+ fatal(message: string, attributes?: Record<string, unknown>): void;
107
+ fmt(strings: TemplateStringsArray, ...values: unknown[]): string;
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Sentry plugin configuration
113
+ */
114
+ export interface SentryPluginConfig {
115
+ /**
116
+ * The Sentry package to use (e.g., '@sentry/node', '@sentry/browser', '@sentry/nextjs')
117
+ * If not provided, the plugin will try to detect based on environment
118
+ */
119
+ sentryPackage?: string;
120
+
121
+ // Core Sentry options
122
+ dsn?: string;
123
+ environment?: string;
124
+ release?: string;
125
+ enabled?: boolean;
126
+ debug?: boolean;
127
+
128
+ // Sampling rates
129
+ tracesSampleRate?: number;
130
+ profilesSampleRate?: number;
131
+ replaysSessionSampleRate?: number;
132
+ replaysOnErrorSampleRate?: number;
133
+
134
+ // Integrations and hooks
135
+ integrations?: any[];
136
+ beforeSend?: (event: any, hint: any) => any;
137
+ beforeSendTransaction?: (event: any, hint: any) => any;
138
+ beforeBreadcrumb?: (breadcrumb: any, hint?: any) => any;
139
+ beforeSendLog?: (log: any) => any;
140
+
141
+ // Logging options
142
+ enableLogs?: boolean;
143
+ consoleLoggingIntegration?: {
144
+ levels?: ('log' | 'warn' | 'error' | 'debug' | 'info' | 'assert' | 'trace')[];
145
+ };
146
+
147
+ // Trace propagation
148
+ tracePropagationTargets?: (string | RegExp)[];
149
+
150
+ // Other options
151
+ initialScope?: any;
152
+ maxBreadcrumbs?: number;
153
+ attachStacktrace?: boolean;
154
+ autoSessionTracking?: boolean;
155
+ sendDefaultPii?: boolean;
156
+ }
157
+
158
+ /**
159
+ * Sentry plugin implementation
160
+ */
161
+ /**
162
+ * Sentry plugin implementation.
163
+ *
164
+ * Integrates Sentry error tracking and performance monitoring into the observability system.
165
+ * Auto-detects the appropriate Sentry package (@sentry/node, @sentry/browser, @sentry/nextjs)
166
+ * based on the runtime environment.
167
+ */
168
+ export class SentryPlugin<
169
+ T extends SentryClient = SentryClient,
170
+ > implements ObservabilityServerPlugin<T> {
171
+ name = 'sentry';
172
+ enabled: boolean;
173
+ protected client: T | undefined;
174
+ protected initialized = false;
175
+ protected sentryPackage: string;
176
+ protected config: SentryPluginConfig;
177
+
178
+ /**
179
+ * Create a new SentryPlugin instance.
180
+ *
181
+ * @param config - Sentry plugin configuration
182
+ */
183
+ constructor(config: SentryPluginConfig = {}) {
184
+ this.config = config;
185
+ const env = safeEnv();
186
+ // Auto-enable if DSN is provided
187
+ const hasDSN = config.dsn ?? env.SENTRY_DSN ?? env.NEXT_PUBLIC_SENTRY_DSN;
188
+ this.enabled = config.enabled ?? Boolean(hasDSN);
189
+
190
+ // Determine Sentry package to use
191
+ this.sentryPackage = config.sentryPackage ?? this.detectSentryPackage();
192
+ }
193
+
194
+ /**
195
+ * Detect which Sentry package to use based on the runtime environment.
196
+ *
197
+ * @returns Package name ('@sentry/node', '@sentry/browser', or '@sentry/nextjs')
198
+ */
199
+ private detectSentryPackage(): string {
200
+ if (isBrowser()) {
201
+ return '@sentry/browser';
202
+ } else if (process.env.NEXT_RUNTIME === 'edge') {
203
+ return '@sentry/nextjs'; // Next.js edge uses same package
204
+ } else if (process.env.NEXT_RUNTIME) {
205
+ return '@sentry/nextjs';
206
+ } else {
207
+ return '@sentry/node';
208
+ }
209
+ }
210
+
211
+ getClient(): T | undefined {
212
+ return this.client;
213
+ }
214
+
215
+ /**
216
+ * Get safe environment access (for testing/mocking).
217
+ *
218
+ * @returns Environment configuration object
219
+ */
220
+ protected getSafeEnv() {
221
+ return safeEnv();
222
+ }
223
+
224
+ async initialize(config?: SentryPluginConfig): Promise<void> {
225
+ if (this.initialized || !this.enabled) return;
226
+
227
+ const env = safeEnv();
228
+ const mergedConfig = { ...this.config, ...config };
229
+
230
+ // Check for DSN
231
+ const dsn = mergedConfig.dsn ?? env.SENTRY_DSN ?? env.NEXT_PUBLIC_SENTRY_DSN;
232
+ if (!dsn) {
233
+ logWarn('Sentry plugin: No DSN provided, skipping initialization');
234
+ this.enabled = false;
235
+ return;
236
+ }
237
+
238
+ try {
239
+ // Dynamic import of the specified Sentry package
240
+ this.client = (await import(/* webpackIgnore: true */ this.sentryPackage)) as T;
241
+
242
+ if (this.client && this.enabled) {
243
+ // Build integrations array if not provided
244
+ let { integrations } = mergedConfig;
245
+ if (!integrations && this.client.browserTracingIntegration) {
246
+ integrations = [];
247
+
248
+ // Add browser tracing for performance monitoring
249
+ if (
250
+ mergedConfig.tracesSampleRate !== undefined ||
251
+ env.SENTRY_TRACES_SAMPLE_RATE !== undefined
252
+ ) {
253
+ integrations.push(this.client.browserTracingIntegration());
254
+ }
255
+
256
+ // Add replay integration if configured
257
+ if (
258
+ this.client.replayIntegration &&
259
+ (mergedConfig.replaysSessionSampleRate !== undefined ||
260
+ env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE !== undefined)
261
+ ) {
262
+ integrations.push(this.client.replayIntegration());
263
+ }
264
+
265
+ // Add profiling if configured
266
+ if (
267
+ this.client.profilesIntegration &&
268
+ (mergedConfig.profilesSampleRate !== undefined ||
269
+ env.SENTRY_PROFILES_SAMPLE_RATE !== undefined)
270
+ ) {
271
+ integrations.push(this.client.profilesIntegration());
272
+ }
273
+ }
274
+
275
+ // Add console logging integration if logs are enabled
276
+ const enableLogs = mergedConfig.enableLogs ?? true;
277
+ if (enableLogs && this.client.consoleLoggingIntegration) {
278
+ const consoleLoggingConfig = mergedConfig.consoleLoggingIntegration || {
279
+ levels: ['log', 'warn', 'error'],
280
+ };
281
+ if (!integrations) {
282
+ integrations = [];
283
+ }
284
+ integrations.push(this.client.consoleLoggingIntegration(consoleLoggingConfig));
285
+ }
286
+
287
+ this.client.init({
288
+ dsn,
289
+ environment: mergedConfig.environment ?? env.SENTRY_ENVIRONMENT ?? process.env.NODE_ENV,
290
+ release: mergedConfig.release ?? env.SENTRY_RELEASE,
291
+ debug: mergedConfig.debug ?? env.SENTRY_DEBUG,
292
+
293
+ // Enable logs to be sent to Sentry
294
+ enableLogs,
295
+
296
+ // Sampling rates
297
+ tracesSampleRate: mergedConfig.tracesSampleRate ?? env.SENTRY_TRACES_SAMPLE_RATE,
298
+ profilesSampleRate: mergedConfig.profilesSampleRate ?? env.SENTRY_PROFILES_SAMPLE_RATE,
299
+ replaysSessionSampleRate:
300
+ mergedConfig.replaysSessionSampleRate ?? env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE,
301
+ replaysOnErrorSampleRate:
302
+ mergedConfig.replaysOnErrorSampleRate ?? env.SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE,
303
+
304
+ // Integrations and hooks
305
+ integrations,
306
+ beforeSend: mergedConfig.beforeSend,
307
+ beforeSendTransaction: mergedConfig.beforeSendTransaction,
308
+ // Use provided beforeSendLog, or default filter if logs are enabled and no custom filter provided
309
+ beforeSendLog:
310
+ mergedConfig.beforeSendLog ||
311
+ (enableLogs ? (log: any) => defaultBeforeSendLog(log, !isBrowser()) : undefined),
312
+
313
+ // Other options
314
+ tracePropagationTargets: mergedConfig.tracePropagationTargets,
315
+ initialScope: mergedConfig.initialScope,
316
+ maxBreadcrumbs: mergedConfig.maxBreadcrumbs,
317
+ attachStacktrace: mergedConfig.attachStacktrace,
318
+ autoSessionTracking: mergedConfig.autoSessionTracking,
319
+ });
320
+
321
+ this.initialized = true;
322
+ }
323
+ } catch (error) {
324
+ logError(`Failed to import Sentry package '${this.sentryPackage}'`, { error });
325
+ this.enabled = false;
326
+ }
327
+ }
328
+
329
+ async shutdown(): Promise<void> {
330
+ if (this.client && this.initialized) {
331
+ // Use close if available, otherwise flush
332
+ if (this.client.close) {
333
+ await this.client.close();
334
+ } else if (this.client.flush) {
335
+ await this.client.flush();
336
+ }
337
+ this.initialized = false;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Clean up the plugin (alias for shutdown)
343
+ */
344
+ async cleanup(): Promise<void> {
345
+ await this.shutdown();
346
+ }
347
+
348
+ captureException(error: Error | unknown, context?: ObservabilityContext): void {
349
+ if (!this.enabled || !this.client) return;
350
+
351
+ // Additional build-time guard to prevent errors during static generation
352
+ if (typeof this.client.captureException !== 'function') {
353
+ // During build/static generation, Sentry client may not be properly initialized
354
+ // Fall back to logging in development
355
+ if (process.env.NODE_ENV === 'development') {
356
+ logError('[Sentry Fallback] Exception', { error });
357
+ }
358
+ return;
359
+ }
360
+
361
+ try {
362
+ this.client.captureException(error, context);
363
+ } catch (captureError) {
364
+ // Graceful fallback if Sentry fails during build
365
+ if (process.env.NODE_ENV === 'development') {
366
+ logWarn('[Sentry Plugin] Failed to capture exception', { error: captureError });
367
+ logError('[Sentry Fallback] Exception', { error });
368
+ }
369
+ }
370
+ }
371
+
372
+ captureMessage(message: string, level: LogLevel = 'info', context?: ObservabilityContext): void {
373
+ if (!this.enabled || !this.client) return;
374
+
375
+ // Prefer Sentry.logger if available (structured logs)
376
+ if (this.client.logger) {
377
+ try {
378
+ const attributes: Record<string, unknown> | undefined = context
379
+ ? ((context.extra as Record<string, unknown> | undefined) ??
380
+ (context as Record<string, unknown>))
381
+ : undefined;
382
+ switch (level) {
383
+ case 'debug':
384
+ this.client.logger.debug(message, attributes);
385
+ return;
386
+ case 'info':
387
+ this.client.logger.info(message, attributes);
388
+ return;
389
+ case 'warning':
390
+ this.client.logger.warn(message, attributes);
391
+ return;
392
+ case 'error':
393
+ this.client.logger.error(message, attributes);
394
+ return;
395
+ }
396
+ } catch (error) {
397
+ // Fall through to captureMessage if logger fails
398
+ if (process.env.NODE_ENV === 'development') {
399
+ logWarn('[Sentry Plugin] Failed to log via logger', { error });
400
+ }
401
+ }
402
+ }
403
+
404
+ // Fallback to captureMessage for compatibility
405
+ // Additional build-time guard to prevent errors during static generation
406
+ if (typeof this.client.captureMessage !== 'function') {
407
+ // During build/static generation, Sentry client may not be properly initialized
408
+ // Fall back to logging in development
409
+ if (process.env.NODE_ENV === 'development') {
410
+ if (level === 'error') {
411
+ logError(`[Sentry Fallback] ${message}`);
412
+ } else if (level === 'warning') {
413
+ logWarn(`[Sentry Fallback] ${message}`);
414
+ } else {
415
+ logDebug(`[Sentry Fallback] ${message}`);
416
+ }
417
+ }
418
+ return;
419
+ }
420
+
421
+ // Map our log levels to Sentry severity levels
422
+ const sentryLevel =
423
+ level === 'warning'
424
+ ? 'warning'
425
+ : level === 'error'
426
+ ? 'error'
427
+ : level === 'debug'
428
+ ? 'debug'
429
+ : 'info';
430
+
431
+ try {
432
+ // Build capture context from ObservabilityContext
433
+ const captureContext = context
434
+ ? {
435
+ level: sentryLevel,
436
+ extra: context.extra ?? context,
437
+ tags: context.tags,
438
+ }
439
+ : sentryLevel;
440
+
441
+ this.client.captureMessage(message, captureContext);
442
+ } catch (error) {
443
+ // Graceful fallback if Sentry fails during build
444
+ if (process.env.NODE_ENV === 'development') {
445
+ logWarn('[Sentry Plugin] Failed to capture message', { error });
446
+ if (level === 'error') {
447
+ logError(`[Sentry Fallback] ${message}`);
448
+ } else if (level === 'warning') {
449
+ logWarn(`[Sentry Fallback] ${message}`);
450
+ } else {
451
+ logDebug(`[Sentry Fallback] ${message}`);
452
+ }
453
+ }
454
+ }
455
+ }
456
+
457
+ setUser(user: ObservabilityUser | null): void {
458
+ if (!this.enabled || !this.client) return;
459
+ this.client.setUser(user);
460
+ }
461
+
462
+ addBreadcrumb(breadcrumb: Breadcrumb): void {
463
+ if (!this.enabled || !this.client) return;
464
+ this.client.addBreadcrumb({
465
+ ...breadcrumb,
466
+ timestamp: breadcrumb.timestamp ?? Date.now() / 1000,
467
+ });
468
+ }
469
+
470
+ withScope(callback: (scope: any) => void): void {
471
+ if (!this.enabled || !this.client) return;
472
+ this.client.withScope(callback);
473
+ }
474
+
475
+ /**
476
+ * Start a new transaction
477
+ */
478
+ startTransaction(
479
+ context: TransactionContext,
480
+ customSamplingContext?: any,
481
+ ): Transaction | undefined {
482
+ if (!this.enabled || !this.client) return undefined;
483
+
484
+ if (this.client.startTransaction) {
485
+ return this.client.startTransaction(context, customSamplingContext);
486
+ }
487
+
488
+ // Fallback for older versions
489
+ logWarn('startTransaction not available in this Sentry version');
490
+ return undefined;
491
+ }
492
+
493
+ /**
494
+ * Start a new span
495
+ */
496
+ startSpan(context: SpanContext): Span | undefined {
497
+ if (!this.enabled || !this.client) return undefined;
498
+
499
+ if (this.client.startSpan) {
500
+ return this.client.startSpan(context);
501
+ }
502
+
503
+ // Try to create span from active transaction
504
+ const transaction = this.getActiveTransaction();
505
+ if (transaction?.startChild) {
506
+ return transaction.startChild(context);
507
+ }
508
+
509
+ return undefined;
510
+ }
511
+
512
+ /**
513
+ * Get the currently active transaction
514
+ */
515
+ getActiveTransaction(): Transaction | undefined {
516
+ if (!this.enabled || !this.client) return undefined;
517
+
518
+ if (this.client.getActiveTransaction) {
519
+ return this.client.getActiveTransaction();
520
+ }
521
+
522
+ // Try to get from current scope
523
+ if (this.client.getCurrentScope) {
524
+ const scope = this.client.getCurrentScope();
525
+ if (scope?.getTransaction) {
526
+ return scope.getTransaction();
527
+ }
528
+ }
529
+
530
+ return undefined;
531
+ }
532
+
533
+ /**
534
+ * Configure the current scope
535
+ */
536
+ configureScope(callback: (scope: Scope) => void): void {
537
+ if (!this.enabled || !this.client) return;
538
+
539
+ if (this.client.configureScope) {
540
+ this.client.configureScope(callback);
541
+ } else if (this.client.withScope) {
542
+ // Fallback using withScope
543
+ this.client.withScope(callback);
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Get the current hub instance
549
+ */
550
+ getCurrentHub(): Hub | undefined {
551
+ if (!this.enabled || !this.client) return undefined;
552
+
553
+ if (this.client.getCurrentHub) {
554
+ return this.client.getCurrentHub();
555
+ }
556
+
557
+ return undefined;
558
+ }
559
+
560
+ /**
561
+ * Set a measurement on the active transaction
562
+ */
563
+ setMeasurement(name: string, value: number, unit?: string): void {
564
+ if (!this.enabled || !this.client) return;
565
+
566
+ const transaction = this.getActiveTransaction();
567
+ if (transaction?.setMeasurement) {
568
+ transaction.setMeasurement(name, value, unit);
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Add performance entries as breadcrumbs and measurements
574
+ */
575
+ addPerformanceEntries(
576
+ entries: Array<{
577
+ entryType: string;
578
+ name: string;
579
+ startTime: number;
580
+ duration: number;
581
+ [key: string]: unknown;
582
+ }>,
583
+ ): void {
584
+ if (!this.enabled || !this.client) return;
585
+
586
+ const transaction = this.getActiveTransaction();
587
+
588
+ entries.forEach(entry => {
589
+ // Add as breadcrumb for context
590
+ this.addBreadcrumb({
591
+ category: 'performance',
592
+ message: `${entry.entryType}: ${entry.name}`,
593
+ data: {
594
+ name: entry.name,
595
+ duration: entry.duration,
596
+ startTime: entry.startTime,
597
+ ...(entry.entryType === 'navigation' && {
598
+ transferSize: (entry as { transferSize?: number }).transferSize,
599
+ encodedBodySize: (entry as { encodedBodySize?: number }).encodedBodySize,
600
+ decodedBodySize: (entry as { decodedBodySize?: number }).decodedBodySize,
601
+ }),
602
+ ...(entry.entryType === 'resource' && {
603
+ initiatorType: (entry as { initiatorType?: string }).initiatorType,
604
+ transferSize: (entry as { transferSize?: number }).transferSize,
605
+ }),
606
+ },
607
+ });
608
+
609
+ // Add measurements for key metrics
610
+ if (transaction && entry.entryType === 'navigation') {
611
+ const navEntry = entry as unknown as {
612
+ responseStart?: number;
613
+ fetchStart?: number;
614
+ domInteractive?: number;
615
+ domComplete?: number;
616
+ loadEventEnd?: number;
617
+ transferSize?: number;
618
+ encodedBodySize?: number;
619
+ decodedBodySize?: number;
620
+ [key: string]: unknown;
621
+ };
622
+
623
+ // Core Web Vitals and other metrics
624
+ const fetchStart = navEntry.fetchStart ?? 0;
625
+ if (navEntry.responseStart !== undefined) {
626
+ this.setMeasurement('fcp', navEntry.responseStart - fetchStart, 'millisecond');
627
+ }
628
+ if (navEntry.domInteractive !== undefined) {
629
+ this.setMeasurement(
630
+ 'dom_interactive',
631
+ navEntry.domInteractive - fetchStart,
632
+ 'millisecond',
633
+ );
634
+ }
635
+ if (navEntry.domComplete !== undefined) {
636
+ this.setMeasurement('dom_complete', navEntry.domComplete - fetchStart, 'millisecond');
637
+ }
638
+ if (navEntry.loadEventEnd !== undefined) {
639
+ this.setMeasurement('load_event_end', navEntry.loadEventEnd - fetchStart, 'millisecond');
640
+ }
641
+ }
642
+ });
643
+ }
644
+
645
+ /**
646
+ * Record a Web Vital measurement
647
+ */
648
+ recordWebVital(
649
+ name: 'FCP' | 'LCP' | 'FID' | 'CLS' | 'TTFB' | 'INP' | string,
650
+ value: number,
651
+ options?: {
652
+ unit?: string;
653
+ rating?: 'good' | 'needs-improvement' | 'poor';
654
+ },
655
+ ): void {
656
+ if (!this.enabled || !this.client) return;
657
+
658
+ const { unit = 'millisecond', rating } = options ?? {};
659
+ const transaction = this.getActiveTransaction();
660
+
661
+ if (transaction) {
662
+ // Set the measurement
663
+ transaction.setMeasurement(name.toLowerCase(), value, unit);
664
+
665
+ // Add rating as tag if provided
666
+ if (rating) {
667
+ transaction.setTag(`webvital.${name.toLowerCase()}.rating`, rating);
668
+ }
669
+
670
+ // Also send as a standalone event for monitoring
671
+ this.client.captureMessage(`Web Vital: ${name}`, {
672
+ level: 'info',
673
+ tags: {
674
+ 'webvital.name': name,
675
+ 'webvital.value': value,
676
+ 'webvital.unit': unit,
677
+ ...(rating && { 'webvital.rating': rating }),
678
+ },
679
+ contexts: {
680
+ trace: {
681
+ trace_id: transaction.traceId,
682
+ span_id: transaction.spanId,
683
+ },
684
+ },
685
+ });
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Create a custom performance mark
691
+ */
692
+ mark(name: string, options?: { detail?: any }): void {
693
+ if (!this.enabled) return;
694
+
695
+ // Use Performance API if available
696
+ if (typeof performance !== 'undefined' && performance.mark) {
697
+ performance.mark(name, options);
698
+ }
699
+
700
+ // Also add as breadcrumb
701
+ this.addBreadcrumb({
702
+ category: 'performance.mark',
703
+ message: name,
704
+ data: options?.detail,
705
+ timestamp: Date.now() / 1000,
706
+ });
707
+ }
708
+
709
+ /**
710
+ * Create a custom performance measure
711
+ */
712
+ measure(
713
+ name: string,
714
+ startMarkOrOptions?:
715
+ | string
716
+ | { start?: string; end?: string; duration?: number; detail?: unknown },
717
+ endMark?: string,
718
+ ): void {
719
+ if (!this.enabled) return;
720
+
721
+ // Use Performance API if available
722
+ if (typeof performance !== 'undefined' && performance.measure) {
723
+ if (typeof startMarkOrOptions === 'string') {
724
+ performance.measure(name, startMarkOrOptions, endMark);
725
+ } else if (startMarkOrOptions) {
726
+ performance.measure(name, startMarkOrOptions);
727
+ } else {
728
+ // No options provided, create a simple measure
729
+ performance.measure(name);
730
+ }
731
+
732
+ // Get the measure to record its duration
733
+ const measures = performance.getEntriesByName(name, 'measure');
734
+ const measure = measures[measures.length - 1];
735
+
736
+ if (measure) {
737
+ this.setMeasurement(name, measure.duration, 'millisecond');
738
+
739
+ this.addBreadcrumb({
740
+ category: 'performance.measure',
741
+ message: name,
742
+ data: {
743
+ duration: measure.duration,
744
+ startTime: measure.startTime,
745
+ },
746
+ timestamp: measure.startTime / 1000,
747
+ });
748
+ }
749
+ }
750
+ }
751
+
752
+ /**
753
+ * Log a trace message using Sentry.logger (if available).
754
+ * Falls back to captureMessage if logger is not available.
755
+ *
756
+ * @param message - Trace message to log
757
+ * @param attributes - Optional attributes to include with the log
758
+ */
759
+ logTrace(message: string, attributes?: Record<string, unknown>): void {
760
+ if (!this.enabled || !this.client) return;
761
+
762
+ if (this.client.logger?.trace) {
763
+ try {
764
+ this.client.logger.trace(message, attributes);
765
+ return;
766
+ } catch (error) {
767
+ if (process.env.NODE_ENV === 'development') {
768
+ logWarn('[Sentry Plugin] Failed to log trace', { error });
769
+ }
770
+ }
771
+ }
772
+
773
+ // Fallback to captureMessage
774
+ this.captureMessage(message, 'debug', attributes);
775
+ }
776
+
777
+ /**
778
+ * Log a debug message using Sentry.logger (if available).
779
+ * Falls back to captureMessage if logger is not available.
780
+ *
781
+ * @param message - Debug message to log
782
+ * @param attributes - Optional attributes to include with the log
783
+ */
784
+ logDebug(message: string, attributes?: Record<string, unknown>): void {
785
+ if (!this.enabled || !this.client) return;
786
+
787
+ if (this.client.logger?.debug) {
788
+ try {
789
+ this.client.logger.debug(message, attributes);
790
+ return;
791
+ } catch (error) {
792
+ if (process.env.NODE_ENV === 'development') {
793
+ logWarn('[Sentry Plugin] Failed to log debug', { error });
794
+ }
795
+ }
796
+ }
797
+
798
+ // Fallback to captureMessage
799
+ this.captureMessage(message, 'debug', attributes);
800
+ }
801
+
802
+ /**
803
+ * Log an info message using Sentry.logger (if available).
804
+ * Falls back to captureMessage if logger is not available.
805
+ *
806
+ * @param message - Info message to log
807
+ * @param attributes - Optional attributes to include with the log
808
+ */
809
+ logInfo(message: string, attributes?: Record<string, unknown>): void {
810
+ if (!this.enabled || !this.client) return;
811
+
812
+ if (this.client.logger?.info) {
813
+ try {
814
+ this.client.logger.info(message, attributes);
815
+ return;
816
+ } catch (error) {
817
+ if (process.env.NODE_ENV === 'development') {
818
+ logWarn('[Sentry Plugin] Failed to log info', { error });
819
+ }
820
+ }
821
+ }
822
+
823
+ // Fallback to captureMessage
824
+ this.captureMessage(message, 'info', attributes);
825
+ }
826
+
827
+ /**
828
+ * Log a warning message using Sentry.logger (if available).
829
+ * Falls back to captureMessage if logger is not available.
830
+ *
831
+ * @param message - Warning message to log
832
+ * @param attributes - Optional attributes to include with the log
833
+ */
834
+ logWarn(message: string, attributes?: Record<string, unknown>): void {
835
+ if (!this.enabled || !this.client) return;
836
+
837
+ if (this.client.logger?.warn) {
838
+ try {
839
+ this.client.logger.warn(message, attributes);
840
+ return;
841
+ } catch (error) {
842
+ if (process.env.NODE_ENV === 'development') {
843
+ logWarn('[Sentry Plugin] Failed to log warn', { error });
844
+ }
845
+ }
846
+ }
847
+
848
+ // Fallback to captureMessage
849
+ this.captureMessage(message, 'warning', attributes);
850
+ }
851
+
852
+ /**
853
+ * Log an error message using Sentry.logger (if available).
854
+ * Falls back to captureMessage if logger is not available.
855
+ *
856
+ * @param message - Error message to log
857
+ * @param attributes - Optional attributes to include with the log
858
+ */
859
+ logError(message: string, attributes?: Record<string, unknown>): void {
860
+ if (!this.enabled || !this.client) return;
861
+
862
+ if (this.client.logger?.error) {
863
+ try {
864
+ this.client.logger.error(message, attributes);
865
+ return;
866
+ } catch (error) {
867
+ if (process.env.NODE_ENV === 'development') {
868
+ logWarn('[Sentry Plugin] Failed to log error', { error });
869
+ }
870
+ }
871
+ }
872
+
873
+ // Fallback to captureMessage
874
+ this.captureMessage(message, 'error', attributes);
875
+ }
876
+
877
+ /**
878
+ * Log a fatal message using Sentry.logger (if available).
879
+ * Falls back to captureMessage if logger is not available.
880
+ *
881
+ * @param message - Fatal message to log
882
+ * @param attributes - Optional attributes to include with the log
883
+ */
884
+ logFatal(message: string, attributes?: Record<string, unknown>): void {
885
+ if (!this.enabled || !this.client) return;
886
+
887
+ if (this.client.logger?.fatal) {
888
+ try {
889
+ this.client.logger.fatal(message, attributes);
890
+ return;
891
+ } catch (error) {
892
+ if (process.env.NODE_ENV === 'development') {
893
+ logWarn('[Sentry Plugin] Failed to log fatal', { error });
894
+ }
895
+ }
896
+ }
897
+
898
+ // Fallback to captureMessage with error level
899
+ this.captureMessage(message, 'error', attributes);
900
+ }
901
+
902
+ /**
903
+ * Get the Sentry.logger instance if available.
904
+ * This allows direct access to Sentry.logger APIs including fmt.
905
+ *
906
+ * @returns Sentry.logger instance or undefined if not available
907
+ *
908
+ * @example
909
+ * ```typescript
910
+ * const logger = sentryPlugin.getLogger();
911
+ * if (logger) {
912
+ * logger.info(logger.fmt`User ${user.id} logged in`);
913
+ * }
914
+ * ```
915
+ */
916
+ getLogger(): SentryClient['logger'] | undefined {
917
+ if (!this.enabled || !this.client) return undefined;
918
+ return this.client.logger;
919
+ }
920
+
921
+ async flush(timeout?: number): Promise<boolean> {
922
+ if (!this.enabled || !this.client || !this.client.flush) return true;
923
+ try {
924
+ return await this.client.flush(timeout);
925
+ } catch (_error) {
926
+ return false;
927
+ }
928
+ }
929
+ }
930
+
931
+ /**
932
+ * Factory function to create a Sentry plugin.
933
+ *
934
+ * Creates a configured SentryPlugin instance with optional configuration.
935
+ * Auto-detects the appropriate Sentry package based on the environment.
936
+ *
937
+ * @param config - Optional Sentry plugin configuration
938
+ * @returns SentryPlugin instance
939
+ *
940
+ * @example
941
+ * ```typescript
942
+ * const sentry = createSentryPlugin({
943
+ * dsn: 'https://...',
944
+ * environment: 'production',
945
+ * tracesSampleRate: 0.1,
946
+ * });
947
+ * ```
948
+ */
949
+ export const createSentryPlugin = <T extends SentryClient = SentryClient>(
950
+ config?: SentryPluginConfig,
951
+ ): SentryPlugin<T> => {
952
+ return new SentryPlugin<T>(config);
953
+ };