@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,511 @@
1
+ /**
2
+ * @fileoverview Tests for Sentry plugin tracing functionality
3
+ * Tests for Sentry plugin tracing functionality
4
+ */
5
+
6
+ import * as SentryObservabilityPlugin from '@integrations/sentry/observability-plugin';
7
+ import { afterEach, beforeEach, describe, expect, vi } from 'vitest';
8
+
9
+ const { SentryPlugin } = SentryObservabilityPlugin;
10
+ type SentryPluginConfig = SentryObservabilityPlugin.SentryPluginConfig;
11
+
12
+ // Mock environment
13
+ vi.mock('@integrations/sentry/observability-plugin', async importOriginal => {
14
+ const actual = await importOriginal<typeof SentryObservabilityPlugin>();
15
+ return {
16
+ ...actual,
17
+ safeEnv: vi.fn(() => ({
18
+ SENTRY_DSN: 'https://key@sentry.io/project',
19
+ SENTRY_ENVIRONMENT: 'test',
20
+ SENTRY_TRACES_SAMPLE_RATE: 1.0,
21
+ })),
22
+ };
23
+ });
24
+
25
+ // Legacy mock kept for reference (no longer used)
26
+ vi.mock('../env', () => ({
27
+ safeEnv: vi.fn(() => ({
28
+ SENTRY_DSN: 'https://key@sentry.io/project',
29
+ SENTRY_ENVIRONMENT: 'test',
30
+ SENTRY_TRACES_SAMPLE_RATE: 1.0,
31
+ })),
32
+ }));
33
+
34
+ // Helper to inject mock client directly into plugin for testing
35
+ function injectMockClient(
36
+ pluginInstance: InstanceType<typeof SentryPlugin>,
37
+ mockClient: any,
38
+ ): void {
39
+ // Access protected properties for testing via type casting
40
+ const pluginAny = pluginInstance as any;
41
+ pluginAny.client = mockClient;
42
+ pluginAny.initialized = true;
43
+ pluginAny.enabled = true;
44
+ }
45
+
46
+ describe('sentryPlugin - Tracing', () => {
47
+ let plugin: InstanceType<typeof SentryPlugin>;
48
+ let mockSentryClient: any;
49
+
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+
53
+ // Mock Sentry client
54
+ mockSentryClient = {
55
+ init: vi.fn(),
56
+ captureException: vi.fn(),
57
+ captureMessage: vi.fn(),
58
+ setUser: vi.fn(),
59
+ addBreadcrumb: vi.fn(),
60
+ withScope: vi.fn(),
61
+ getCurrentScope: vi.fn(),
62
+ getActiveTransaction: vi.fn(),
63
+ startTransaction: vi.fn(),
64
+ startSpan: vi.fn(),
65
+ configureScope: vi.fn(),
66
+ getCurrentHub: vi.fn(),
67
+ flush: vi.fn(),
68
+ close: vi.fn(),
69
+ browserTracingIntegration: vi.fn(() => ({ name: 'BrowserTracing' })),
70
+ replayIntegration: vi.fn(() => ({ name: 'Replay' })),
71
+ profilesIntegration: vi.fn(() => ({ name: 'Profiling' })),
72
+ };
73
+
74
+ const config: SentryPluginConfig = {
75
+ sentryPackage: '@sentry/nextjs',
76
+ dsn: 'https://key@sentry.io/project',
77
+ };
78
+
79
+ plugin = new SentryPlugin(config);
80
+ });
81
+
82
+ afterEach(() => {
83
+ vi.resetAllMocks();
84
+ });
85
+
86
+ describe('transaction Management', () => {
87
+ beforeEach(async () => {
88
+ // Inject mock client directly instead of relying on dynamic import mocking
89
+ injectMockClient(plugin, mockSentryClient);
90
+ });
91
+
92
+ it('should start transaction', () => {
93
+ const mockTransaction = {
94
+ setMeasurement: vi.fn(),
95
+ setTag: vi.fn(),
96
+ finish: vi.fn(),
97
+ startChild: vi.fn(),
98
+ };
99
+
100
+ mockSentryClient.startTransaction.mockReturnValue(mockTransaction);
101
+
102
+ const context = {
103
+ name: 'test-transaction',
104
+ op: 'http.server',
105
+ data: { method: 'GET' },
106
+ };
107
+
108
+ const result = plugin.startTransaction(context);
109
+
110
+ expect(result).toBe(mockTransaction);
111
+ expect(mockSentryClient.startTransaction).toHaveBeenCalledWith(context, undefined);
112
+ });
113
+
114
+ it('should start transaction with custom sampling context', () => {
115
+ const mockTransaction = { finish: vi.fn() };
116
+ mockSentryClient.startTransaction.mockReturnValue(mockTransaction);
117
+
118
+ const context = { name: 'test', op: 'custom' };
119
+ const samplingContext = { request: { method: 'POST' } };
120
+
121
+ plugin.startTransaction(context, samplingContext);
122
+
123
+ expect(mockSentryClient.startTransaction).toHaveBeenCalledWith(context, samplingContext);
124
+ });
125
+
126
+ it('should handle missing startTransaction method', () => {
127
+ mockSentryClient.startTransaction = undefined;
128
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
129
+
130
+ const result = plugin.startTransaction({ name: 'test' });
131
+
132
+ expect(result).toBeUndefined();
133
+ expect(consoleSpy).toHaveBeenCalledWith(
134
+ '[node]',
135
+ 'startTransaction not available in this Sentry version',
136
+ );
137
+ });
138
+
139
+ it('should get active transaction', () => {
140
+ const mockTransaction = { name: 'active-transaction' };
141
+ mockSentryClient.getActiveTransaction.mockReturnValue(mockTransaction);
142
+
143
+ const result = plugin.getActiveTransaction();
144
+
145
+ expect(result).toBe(mockTransaction);
146
+ expect(mockSentryClient.getActiveTransaction).toHaveBeenCalledWith();
147
+ });
148
+
149
+ it('should get active transaction from scope fallback', () => {
150
+ const mockTransaction = { name: 'scope-transaction' };
151
+ const mockScope = { getTransaction: vi.fn(() => mockTransaction) };
152
+
153
+ mockSentryClient.getActiveTransaction = undefined;
154
+ mockSentryClient.getCurrentScope.mockReturnValue(mockScope);
155
+
156
+ const result = plugin.getActiveTransaction();
157
+
158
+ expect(result).toBe(mockTransaction);
159
+ expect(mockScope.getTransaction).toHaveBeenCalledWith();
160
+ });
161
+ });
162
+
163
+ describe('span Management', () => {
164
+ beforeEach(async () => {
165
+ injectMockClient(plugin, mockSentryClient);
166
+ });
167
+
168
+ it('should start span', () => {
169
+ const mockSpan = { finish: vi.fn() };
170
+ mockSentryClient.startSpan.mockReturnValue(mockSpan);
171
+
172
+ const context = {
173
+ name: 'test-span',
174
+ op: 'db.query',
175
+ data: { table: 'users' },
176
+ };
177
+
178
+ const result = plugin.startSpan(context);
179
+
180
+ expect(result).toBe(mockSpan);
181
+ expect(mockSentryClient.startSpan).toHaveBeenCalledWith(context);
182
+ });
183
+
184
+ it('should start span from active transaction fallback', () => {
185
+ const mockSpan = { finish: vi.fn() };
186
+ const mockTransaction = { startChild: vi.fn(() => mockSpan) };
187
+
188
+ mockSentryClient.startSpan = undefined;
189
+ mockSentryClient.getActiveTransaction.mockReturnValue(mockTransaction);
190
+
191
+ const context = { name: 'child-span' };
192
+ const result = plugin.startSpan(context);
193
+
194
+ expect(result).toBe(mockSpan);
195
+ expect(mockTransaction.startChild).toHaveBeenCalledWith(context);
196
+ });
197
+
198
+ it('should return undefined when no span creation method available', () => {
199
+ mockSentryClient.startSpan = undefined;
200
+ mockSentryClient.getActiveTransaction.mockReturnValue(null);
201
+
202
+ const result = plugin.startSpan({ name: 'test' });
203
+
204
+ expect(result).toBeUndefined();
205
+ });
206
+ });
207
+
208
+ describe('scope Management', () => {
209
+ beforeEach(async () => {
210
+ injectMockClient(plugin, mockSentryClient);
211
+ });
212
+
213
+ it('should configure scope', () => {
214
+ const callback = vi.fn();
215
+
216
+ plugin.configureScope(callback);
217
+
218
+ expect(mockSentryClient.configureScope).toHaveBeenCalledWith(callback);
219
+ });
220
+
221
+ it('should use withScope fallback', () => {
222
+ const callback = vi.fn();
223
+ mockSentryClient.configureScope = undefined;
224
+
225
+ plugin.configureScope(callback);
226
+
227
+ expect(mockSentryClient.withScope).toHaveBeenCalledWith(callback);
228
+ });
229
+
230
+ it('should get current hub', () => {
231
+ const mockHub = { getScope: vi.fn() };
232
+ mockSentryClient.getCurrentHub.mockReturnValue(mockHub);
233
+
234
+ const result = plugin.getCurrentHub();
235
+
236
+ expect(result).toBe(mockHub);
237
+ expect(mockSentryClient.getCurrentHub).toHaveBeenCalledWith();
238
+ });
239
+ });
240
+
241
+ describe('performance Measurements', () => {
242
+ let mockTransaction: any;
243
+
244
+ beforeEach(async () => {
245
+ injectMockClient(plugin, mockSentryClient);
246
+ mockTransaction = {
247
+ setMeasurement: vi.fn(),
248
+ setTag: vi.fn(),
249
+ setData: vi.fn(),
250
+ traceId: 'trace-123',
251
+ spanId: 'span-456',
252
+ };
253
+ mockSentryClient.getActiveTransaction.mockReturnValue(mockTransaction);
254
+ });
255
+
256
+ it('should set measurement on active transaction', () => {
257
+ plugin.setMeasurement('response_time', 150, 'millisecond');
258
+
259
+ expect(mockTransaction.setMeasurement).toHaveBeenCalledWith(
260
+ 'response_time',
261
+ 150,
262
+ 'millisecond',
263
+ );
264
+ });
265
+
266
+ it('should handle no active transaction', () => {
267
+ mockSentryClient.getActiveTransaction.mockReturnValue(null);
268
+
269
+ plugin.setMeasurement('response_time', 150);
270
+
271
+ expect(mockTransaction.setMeasurement).not.toHaveBeenCalled();
272
+ });
273
+
274
+ it('should record web vital', () => {
275
+ plugin.recordWebVital('LCP', 2500, {
276
+ unit: 'millisecond',
277
+ rating: 'good',
278
+ });
279
+
280
+ expect(mockTransaction.setMeasurement).toHaveBeenCalledWith('lcp', 2500, 'millisecond');
281
+ expect(mockTransaction.setTag).toHaveBeenCalledWith('webvital.lcp.rating', 'good');
282
+ expect(mockSentryClient.captureMessage).toHaveBeenCalledWith('Web Vital: LCP', {
283
+ level: 'info',
284
+ tags: {
285
+ 'webvital.name': 'LCP',
286
+ 'webvital.value': 2500,
287
+ 'webvital.unit': 'millisecond',
288
+ 'webvital.rating': 'good',
289
+ },
290
+ contexts: {
291
+ trace: {
292
+ trace_id: 'trace-123',
293
+ span_id: 'span-456',
294
+ },
295
+ },
296
+ });
297
+ });
298
+
299
+ it('should add performance entries', () => {
300
+ const entries: PerformanceEntry[] = [
301
+ {
302
+ name: 'navigation',
303
+ entryType: 'navigation',
304
+ startTime: 0,
305
+ duration: 1500,
306
+ } as PerformanceNavigationTiming,
307
+ {
308
+ name: 'https://example.com/script.js',
309
+ entryType: 'resource',
310
+ startTime: 100,
311
+ duration: 200,
312
+ } as PerformanceResourceTiming,
313
+ ];
314
+
315
+ plugin.addPerformanceEntries(
316
+ entries as unknown as Array<{
317
+ entryType: string;
318
+ name: string;
319
+ startTime: number;
320
+ duration: number;
321
+ [key: string]: unknown;
322
+ }>,
323
+ );
324
+
325
+ expect(mockSentryClient.addBreadcrumb).toHaveBeenCalledTimes(2);
326
+
327
+ // Check navigation entry breadcrumb
328
+ expect(mockSentryClient.addBreadcrumb).toHaveBeenNthCalledWith(
329
+ 1,
330
+ expect.objectContaining({
331
+ category: 'performance',
332
+ message: 'navigation: navigation',
333
+ data: expect.objectContaining({
334
+ name: 'navigation',
335
+ duration: 1500,
336
+ startTime: 0,
337
+ }),
338
+ }),
339
+ );
340
+
341
+ // Check resource entry breadcrumb
342
+ expect(mockSentryClient.addBreadcrumb).toHaveBeenNthCalledWith(
343
+ 2,
344
+ expect.objectContaining({
345
+ category: 'performance',
346
+ message: 'resource: https://example.com/script.js',
347
+ data: expect.objectContaining({
348
+ name: 'https://example.com/script.js',
349
+ duration: 200,
350
+ startTime: 100,
351
+ }),
352
+ }),
353
+ );
354
+ });
355
+ });
356
+
357
+ describe('performance API Integration', () => {
358
+ beforeEach(async () => {
359
+ injectMockClient(plugin, mockSentryClient);
360
+ });
361
+
362
+ it('should create performance mark', () => {
363
+ const mockPerformance = {
364
+ mark: vi.fn(),
365
+ };
366
+ Object.defineProperty(global, 'performance', {
367
+ value: mockPerformance,
368
+ configurable: true,
369
+ });
370
+
371
+ plugin.mark('test-mark', { detail: { step: 1 } });
372
+
373
+ expect(mockPerformance.mark).toHaveBeenCalledWith('test-mark', {
374
+ detail: { step: 1 },
375
+ });
376
+ expect(mockSentryClient.addBreadcrumb).toHaveBeenCalledWith({
377
+ category: 'performance.mark',
378
+ message: 'test-mark',
379
+ data: { step: 1 },
380
+ timestamp: expect.any(Number),
381
+ });
382
+ });
383
+
384
+ it('should create performance measure', () => {
385
+ const mockMeasure = {
386
+ name: 'test-measure',
387
+ duration: 150,
388
+ startTime: 1000,
389
+ };
390
+
391
+ const mockPerformance = {
392
+ measure: vi.fn(),
393
+ getEntriesByName: vi.fn(() => [mockMeasure]),
394
+ };
395
+
396
+ Object.defineProperty(global, 'performance', {
397
+ value: mockPerformance,
398
+ configurable: true,
399
+ });
400
+
401
+ const mockTransaction = {
402
+ setMeasurement: vi.fn(),
403
+ };
404
+ mockSentryClient.getActiveTransaction.mockReturnValue(mockTransaction);
405
+
406
+ plugin.measure('test-measure', 'start-mark', 'end-mark');
407
+
408
+ expect(mockPerformance.measure).toHaveBeenCalledWith(
409
+ 'test-measure',
410
+ 'start-mark',
411
+ 'end-mark',
412
+ );
413
+ expect(mockTransaction.setMeasurement).toHaveBeenCalledWith(
414
+ 'test-measure',
415
+ 150,
416
+ 'millisecond',
417
+ );
418
+ expect(mockSentryClient.addBreadcrumb).toHaveBeenCalledWith({
419
+ category: 'performance.measure',
420
+ message: 'test-measure',
421
+ data: {
422
+ duration: 150,
423
+ startTime: 1000,
424
+ },
425
+ timestamp: 1,
426
+ });
427
+ });
428
+
429
+ it('should handle performance measure with options', () => {
430
+ const mockMeasure = {
431
+ name: 'test-measure',
432
+ duration: 100,
433
+ startTime: 2000,
434
+ };
435
+
436
+ const mockPerformance = {
437
+ measure: vi.fn(),
438
+ getEntriesByName: vi.fn(() => [mockMeasure]),
439
+ };
440
+
441
+ Object.defineProperty(global, 'performance', {
442
+ value: mockPerformance,
443
+ configurable: true,
444
+ });
445
+
446
+ const options = { start: '1000', end: '1100' };
447
+ plugin.measure('test-measure', options);
448
+
449
+ expect(mockPerformance.measure).toHaveBeenCalledWith('test-measure', options);
450
+ });
451
+ });
452
+
453
+ describe('plugin State Management', () => {
454
+ it('should not perform tracing operations when disabled', () => {
455
+ plugin.enabled = false;
456
+
457
+ const result = plugin.startTransaction({ name: 'test' });
458
+ expect(result).toBeUndefined();
459
+
460
+ const span = plugin.startSpan({ name: 'test' });
461
+ expect(span).toBeUndefined();
462
+
463
+ plugin.setMeasurement('test', 100);
464
+ plugin.recordWebVital('LCP', 2500);
465
+ plugin.mark('test-mark');
466
+ plugin.measure('test-measure');
467
+
468
+ expect(mockSentryClient.startTransaction).not.toHaveBeenCalled();
469
+ expect(mockSentryClient.startSpan).not.toHaveBeenCalled();
470
+ });
471
+
472
+ it('should not perform operations when client is not available', async () => {
473
+ plugin = new SentryPlugin({ dsn: 'invalid-dsn' });
474
+ // Don't initialize to keep client undefined
475
+
476
+ const result = plugin.startTransaction({ name: 'test' });
477
+ expect(result).toBeUndefined();
478
+ });
479
+ });
480
+
481
+ describe('flush and Cleanup', () => {
482
+ beforeEach(async () => {
483
+ injectMockClient(plugin, mockSentryClient);
484
+ });
485
+
486
+ it('should flush client', async () => {
487
+ mockSentryClient.flush.mockResolvedValue(true);
488
+
489
+ const result = await plugin.flush(5000);
490
+
491
+ expect(result).toBeTruthy();
492
+ expect(mockSentryClient.flush).toHaveBeenCalledWith(5000);
493
+ });
494
+
495
+ it('should handle flush errors', async () => {
496
+ mockSentryClient.flush.mockRejectedValue(new Error('Flush failed'));
497
+
498
+ const result = await plugin.flush();
499
+
500
+ expect(result).toBeFalsy();
501
+ });
502
+
503
+ it('should return true when flush is not available', async () => {
504
+ mockSentryClient.flush = undefined;
505
+
506
+ const result = await plugin.flush();
507
+
508
+ expect(result).toBeTruthy();
509
+ });
510
+ });
511
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @fileoverview Sentry-specific environment configuration
3
+ * Sentry-specific environment configuration
4
+ * Apps can extend this configuration to inherit Sentry environment variables
5
+ */
6
+
7
+ import { logWarn } from '@repo/shared/logger';
8
+ import { createEnv } from '@t3-oss/env-core';
9
+ import { z } from 'zod/v4';
10
+
11
+ // Create validated env object
12
+ export const env = createEnv({
13
+ server: {
14
+ SENTRY_DSN: z.string().url().optional(),
15
+ SENTRY_AUTH_TOKEN: z.string().optional(),
16
+ SENTRY_ORG: z.string().optional(),
17
+ SENTRY_PROJECT: z.string().optional(),
18
+ SENTRY_ENVIRONMENT: z.enum(['development', 'preview', 'production']).optional(),
19
+ SENTRY_RELEASE: z.string().optional(),
20
+ SENTRY_TRACES_SAMPLE_RATE: z.coerce.number().min(0).max(1).optional(),
21
+ SENTRY_PROFILES_SAMPLE_RATE: z.coerce.number().min(0).max(1).optional(),
22
+ SENTRY_REPLAYS_SESSION_SAMPLE_RATE: z.coerce.number().min(0).max(1).optional(),
23
+ SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: z.coerce.number().min(0).max(1).optional(),
24
+ SENTRY_DEBUG: z
25
+ .string()
26
+ .optional()
27
+ .transform(val => val === 'true')
28
+ .default(false),
29
+ },
30
+ clientPrefix: 'NEXT_PUBLIC_',
31
+ client: {
32
+ NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(),
33
+ NEXT_PUBLIC_SENTRY_ENVIRONMENT: z.enum(['development', 'preview', 'production']).optional(),
34
+ NEXT_PUBLIC_SENTRY_RELEASE: z.string().optional(),
35
+ },
36
+ runtimeEnv: process.env,
37
+ emptyStringAsUndefined: true,
38
+ onInvalidAccess: (variable: string) => {
39
+ throw new Error(
40
+ `❌ Attempted to access a server-side environment variable on the client: ${variable}`,
41
+ );
42
+ },
43
+ onValidationError: error => {
44
+ // Note: Using logWarn during env validation - shared logger is always available
45
+ logWarn('Sentry environment validation failed', { error });
46
+ // Don't throw in packages - use fallbacks
47
+ return undefined as never;
48
+ },
49
+ });
50
+
51
+ /**
52
+ * Safe environment access for non-Next.js contexts.
53
+ *
54
+ * Provides fallback environment variable access for Node.js, workers, and test environments
55
+ * where the validated env object may not be available. Returns fallback values with proper
56
+ * type coercion and defaults.
57
+ *
58
+ * @returns Environment object with Sentry configuration values
59
+ */
60
+ export function safeEnv() {
61
+ if (env) return env;
62
+
63
+ // Fallback values for resilience
64
+ return {
65
+ // Server
66
+ SENTRY_DSN: process.env.SENTRY_DSN ?? '',
67
+ SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN ?? '',
68
+ SENTRY_ORG: process.env.SENTRY_ORG ?? '',
69
+ SENTRY_PROJECT: process.env.SENTRY_PROJECT ?? '',
70
+ SENTRY_ENVIRONMENT:
71
+ (process.env.SENTRY_ENVIRONMENT as 'development' | 'preview' | 'production') ?? 'development',
72
+ SENTRY_RELEASE: process.env.SENTRY_RELEASE ?? '',
73
+ SENTRY_TRACES_SAMPLE_RATE:
74
+ Number(process.env.SENTRY_TRACES_SAMPLE_RATE) ||
75
+ (process.env.NODE_ENV === 'production' ? 0.1 : 1.0),
76
+ SENTRY_PROFILES_SAMPLE_RATE: Number(process.env.SENTRY_PROFILES_SAMPLE_RATE) ?? 0,
77
+ SENTRY_REPLAYS_SESSION_SAMPLE_RATE:
78
+ Number(process.env.SENTRY_REPLAYS_SESSION_SAMPLE_RATE) ||
79
+ (process.env.NODE_ENV === 'production' ? 0.1 : 0),
80
+ SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE:
81
+ Number(process.env.SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE) || 1.0,
82
+ SENTRY_DEBUG: process.env.SENTRY_DEBUG === 'true',
83
+ // Client
84
+ NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN ?? '',
85
+ NEXT_PUBLIC_SENTRY_ENVIRONMENT:
86
+ (process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT as 'development' | 'preview' | 'production') ??
87
+ 'development',
88
+ NEXT_PUBLIC_SENTRY_RELEASE: process.env.NEXT_PUBLIC_SENTRY_RELEASE ?? '',
89
+ };
90
+ }
91
+
92
+ // Export type
93
+ export type Env = typeof env;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @fileoverview Sentry plugin exports
3
+ * Sentry plugin exports
4
+ * Provides full access to @sentry/nextjs while wrapping in plugin interface
5
+ */
6
+
7
+ // Re-export everything from @sentry/nextjs for full Next.js access
8
+ export * from '@sentry/nextjs';
9
+
10
+ // Export plugin and factory
11
+ export { SentryPlugin, createSentryPlugin, defaultBeforeSendLog } from './plugin';
12
+ export type { SentryPluginConfig } from './plugin';
13
+
14
+ // Re-export Sentry environment configuration
15
+ export { env, safeEnv } from './env';
16
+ export type { Env } from './env';
17
+
18
+ // Export types
19
+ export type { ObservabilityPlugin, ObservabilityServerPlugin } from '../../core/plugin';
20
+ export type {
21
+ Hub,
22
+ Scope,
23
+ Span,
24
+ SpanContext,
25
+ SpanStatus,
26
+ Transaction,
27
+ TransactionContext,
28
+ } from './types';