@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.
- package/README.md +523 -0
- package/dist/client-next.d.mts +20 -0
- package/dist/client-next.d.mts.map +1 -0
- package/dist/client-next.mjs +64 -0
- package/dist/client-next.mjs.map +1 -0
- package/dist/client.d.mts +11 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +47 -0
- package/dist/client.mjs.map +1 -0
- package/dist/env.d.mts +15 -0
- package/dist/env.d.mts.map +1 -0
- package/dist/env.mjs +45 -0
- package/dist/env.mjs.map +1 -0
- package/dist/factory-DkY353r8.mjs +380 -0
- package/dist/factory-DkY353r8.mjs.map +1 -0
- package/dist/hooks-useObservability.d.mts +11 -0
- package/dist/hooks-useObservability.d.mts.map +1 -0
- package/dist/hooks-useObservability.mjs +174 -0
- package/dist/hooks-useObservability.mjs.map +1 -0
- package/dist/index-CpcdzWrF.d.mts +24 -0
- package/dist/index-CpcdzWrF.d.mts.map +1 -0
- package/dist/index.d.mts +88 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +97 -0
- package/dist/index.mjs.map +1 -0
- package/dist/manager-BxQqOPEg.d.mts +33 -0
- package/dist/manager-BxQqOPEg.d.mts.map +1 -0
- package/dist/plugin-Bfq-o3nr.d.mts +60 -0
- package/dist/plugin-Bfq-o3nr.d.mts.map +1 -0
- package/dist/plugin-Bt-ygG1m.d.mts +254 -0
- package/dist/plugin-Bt-ygG1m.d.mts.map +1 -0
- package/dist/plugin-CLFwRERa.mjs +593 -0
- package/dist/plugin-CLFwRERa.mjs.map +1 -0
- package/dist/plugin-CP895lBx.mjs +534 -0
- package/dist/plugin-CP895lBx.mjs.map +1 -0
- package/dist/plugin-CaQxviDs.d.mts +61 -0
- package/dist/plugin-CaQxviDs.d.mts.map +1 -0
- package/dist/plugin-lPdJirTY.mjs +234 -0
- package/dist/plugin-lPdJirTY.mjs.map +1 -0
- package/dist/plugins-betterstack-env.d.mts +29 -0
- package/dist/plugins-betterstack-env.d.mts.map +1 -0
- package/dist/plugins-betterstack-env.mjs +75 -0
- package/dist/plugins-betterstack-env.mjs.map +1 -0
- package/dist/plugins-betterstack.d.mts +4 -0
- package/dist/plugins-betterstack.mjs +4 -0
- package/dist/plugins-console.d.mts +37 -0
- package/dist/plugins-console.d.mts.map +1 -0
- package/dist/plugins-console.mjs +196 -0
- package/dist/plugins-console.mjs.map +1 -0
- package/dist/plugins-sentry-env.d.mts +37 -0
- package/dist/plugins-sentry-env.d.mts.map +1 -0
- package/dist/plugins-sentry-env.mjs +79 -0
- package/dist/plugins-sentry-env.mjs.map +1 -0
- package/dist/plugins-sentry-microfrontend-env.d.mts +49 -0
- package/dist/plugins-sentry-microfrontend-env.d.mts.map +1 -0
- package/dist/plugins-sentry-microfrontend-env.mjs +80 -0
- package/dist/plugins-sentry-microfrontend-env.mjs.map +1 -0
- package/dist/plugins-sentry-microfrontend.d.mts +2 -0
- package/dist/plugins-sentry-microfrontend.mjs +3 -0
- package/dist/plugins-sentry.d.mts +5 -0
- package/dist/plugins-sentry.mjs +6 -0
- package/dist/server-edge.d.mts +15 -0
- package/dist/server-edge.d.mts.map +1 -0
- package/dist/server-edge.mjs +53 -0
- package/dist/server-edge.mjs.map +1 -0
- package/dist/server-next.d.mts +17 -0
- package/dist/server-next.d.mts.map +1 -0
- package/dist/server-next.mjs +64 -0
- package/dist/server-next.mjs.map +1 -0
- package/dist/server.d.mts +11 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +48 -0
- package/dist/server.mjs.map +1 -0
- package/dist/utils-CuGrTcD6.d.mts +77 -0
- package/dist/utils-CuGrTcD6.d.mts.map +1 -0
- package/env.ts +67 -0
- package/package.json +147 -0
- package/src/client-next.ts +131 -0
- package/src/client.ts +70 -0
- package/src/core/index.ts +15 -0
- package/src/core/manager.ts +361 -0
- package/src/core/plugin.ts +61 -0
- package/src/core/types.ts +151 -0
- package/src/factory/builder.ts +132 -0
- package/src/factory/index.ts +67 -0
- package/src/factory/presets.ts +78 -0
- package/src/hooks/useObservability.ts +206 -0
- package/src/plugins/betterstack/env.ts +101 -0
- package/src/plugins/betterstack/index.ts +15 -0
- package/src/plugins/betterstack/plugin.ts +373 -0
- package/src/plugins/console/index.ts +323 -0
- package/src/plugins/sentry/__tests__/plugin-tracing.test.ts +511 -0
- package/src/plugins/sentry/env.ts +93 -0
- package/src/plugins/sentry/index.ts +28 -0
- package/src/plugins/sentry/plugin.ts +953 -0
- package/src/plugins/sentry/types.ts +252 -0
- package/src/plugins/sentry-microfrontend/env.ts +105 -0
- package/src/plugins/sentry-microfrontend/index.ts +12 -0
- package/src/plugins/sentry-microfrontend/multiplexed-transport.ts +221 -0
- package/src/plugins/sentry-microfrontend/plugin.ts +500 -0
- package/src/plugins/sentry-microfrontend/sentry-types.ts +140 -0
- package/src/plugins/sentry-microfrontend/types.ts +130 -0
- package/src/plugins/sentry-microfrontend/utils.ts +326 -0
- package/src/server-edge.ts +113 -0
- package/src/server-next.ts +114 -0
- package/src/server.ts +71 -0
- 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';
|