@getmicdrop/svelte-components 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +6 -0
- package/dist/config.spec.d.ts +2 -0
- package/dist/config.spec.d.ts.map +1 -0
- package/dist/config.spec.js +29 -0
- package/dist/constants/formOptions.d.ts +18 -0
- package/dist/constants/formOptions.d.ts.map +1 -0
- package/dist/constants/formOptions.js +25 -0
- package/dist/constants/formOptions.spec.d.ts +2 -0
- package/dist/constants/formOptions.spec.d.ts.map +1 -0
- package/dist/constants/formOptions.spec.js +88 -0
- package/dist/telemetry.d.ts +13 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +358 -0
- package/dist/telemetry.server.d.ts +11 -0
- package/dist/telemetry.server.d.ts.map +1 -0
- package/dist/telemetry.server.js +212 -0
- package/dist/telemetry.server.spec.d.ts +2 -0
- package/dist/telemetry.server.spec.d.ts.map +1 -0
- package/dist/telemetry.server.spec.js +434 -0
- package/dist/telemetry.spec.d.ts +2 -0
- package/dist/telemetry.spec.d.ts.map +1 -0
- package/dist/telemetry.spec.js +660 -0
- package/package.json +153 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Server-side OpenTelemetry instrumentation for SvelteKit Performers Portal
|
|
2
|
+
import { registerInstrumentations } from '@opentelemetry/instrumentation';
|
|
3
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
4
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
5
|
+
import { Resource } from '@opentelemetry/resources';
|
|
6
|
+
import { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION, SEMRESATTRS_DEPLOYMENT_ENVIRONMENT } from '@opentelemetry/semantic-conventions';
|
|
7
|
+
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
|
8
|
+
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
|
|
9
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
|
|
10
|
+
import { trace, context, propagation } from '@opentelemetry/api';
|
|
11
|
+
|
|
12
|
+
// Server telemetry configuration
|
|
13
|
+
const SERVER_TELEMETRY_CONFIG = {
|
|
14
|
+
serviceName: process.env.VITE_OTEL_SERVICE_NAME || 'jetbook-performersportal-ssr',
|
|
15
|
+
serviceVersion: process.env.VITE_OTEL_SERVICE_VERSION || '1.0.0',
|
|
16
|
+
environment: process.env.VITE_ENVIRONMENT || 'development',
|
|
17
|
+
otlpEndpoint: process.env.VITE_OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
|
|
18
|
+
tracesEnabled: process.env.VITE_OTEL_TRACES_ENABLED !== 'false',
|
|
19
|
+
sampleRate: parseFloat(process.env.VITE_OTEL_TRACES_SAMPLER_ARG || '0.1'),
|
|
20
|
+
consoleDebug: process.env.VITE_OTEL_DEBUG === 'true'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let sdk = null;
|
|
24
|
+
|
|
25
|
+
// Initialize server-side telemetry
|
|
26
|
+
export function initServerTelemetry() {
|
|
27
|
+
if (!SERVER_TELEMETRY_CONFIG.tracesEnabled) {
|
|
28
|
+
console.log('[Server Telemetry] Tracing is disabled');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
sdk = new NodeSDK({
|
|
34
|
+
resource: new Resource({
|
|
35
|
+
[SEMRESATTRS_SERVICE_NAME]: SERVER_TELEMETRY_CONFIG.serviceName,
|
|
36
|
+
[SEMRESATTRS_SERVICE_VERSION]: SERVER_TELEMETRY_CONFIG.serviceVersion,
|
|
37
|
+
[SEMRESATTRS_DEPLOYMENT_ENVIRONMENT]: SERVER_TELEMETRY_CONFIG.environment,
|
|
38
|
+
'portal.type': 'performers'
|
|
39
|
+
}),
|
|
40
|
+
traceExporter: new OTLPTraceExporter({
|
|
41
|
+
url: SERVER_TELEMETRY_CONFIG.otlpEndpoint,
|
|
42
|
+
headers: {
|
|
43
|
+
'Content-Type': 'application/json'
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
instrumentations: [
|
|
47
|
+
getNodeAutoInstrumentations({
|
|
48
|
+
'@opentelemetry/instrumentation-fs': {
|
|
49
|
+
enabled: false // Disable fs instrumentation as it's very noisy
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
new HttpInstrumentation({
|
|
53
|
+
requestHook: (span, request) => {
|
|
54
|
+
span.setAttributes({
|
|
55
|
+
'http.route': request.url,
|
|
56
|
+
'http.user_agent': request.headers?.['user-agent'],
|
|
57
|
+
'http.referer': request.headers?.['referer'],
|
|
58
|
+
'ssr.type': 'request',
|
|
59
|
+
'portal.type': 'performers'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
new FetchInstrumentation({
|
|
64
|
+
propagateTraceHeaderCorsUrls: [
|
|
65
|
+
/^http:\/\/localhost:8080\/.*/,
|
|
66
|
+
/^https:\/\/api\.jetbook\.com\/.*/,
|
|
67
|
+
/^https:\/\/.*\.get-micdrop\.com\/.*/
|
|
68
|
+
]
|
|
69
|
+
})
|
|
70
|
+
]
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
sdk.start();
|
|
74
|
+
console.log(`[Server Telemetry] Initialized with ${SERVER_TELEMETRY_CONFIG.sampleRate * 100}% sampling rate`);
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('[Server Telemetry] Failed to initialize:', error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Create spans for SSR page loads and data fetching
|
|
82
|
+
export function createSSRSpan(name, request, attributes = {}) {
|
|
83
|
+
const tracer = trace.getTracer(
|
|
84
|
+
SERVER_TELEMETRY_CONFIG.serviceName,
|
|
85
|
+
SERVER_TELEMETRY_CONFIG.serviceVersion
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return tracer.startSpan(name, {
|
|
89
|
+
attributes: {
|
|
90
|
+
'span.kind': 'server',
|
|
91
|
+
'ssr.type': 'page_load',
|
|
92
|
+
'portal.type': 'performers',
|
|
93
|
+
'http.method': request?.method || 'GET',
|
|
94
|
+
'http.url': request?.url,
|
|
95
|
+
'http.user_agent': request?.headers?.['user-agent'],
|
|
96
|
+
'http.referer': request?.headers?.['referer'],
|
|
97
|
+
...attributes
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Track SSR data loading for performers
|
|
103
|
+
export function trackPerformerSSRDataLoad(loadFunction, context) {
|
|
104
|
+
return async (event) => {
|
|
105
|
+
const span = createSSRSpan(`ssr.performer.load.${context}`, event.request, {
|
|
106
|
+
'ssr.context': context,
|
|
107
|
+
'route.id': event.route?.id,
|
|
108
|
+
'performer.context': true
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const result = await loadFunction(event);
|
|
113
|
+
span.setStatus({ code: 1 }); // OK
|
|
114
|
+
|
|
115
|
+
// Add performer-specific attributes if available
|
|
116
|
+
if (result?.performer) {
|
|
117
|
+
span.setAttributes({
|
|
118
|
+
'performer.id': result.performer.id,
|
|
119
|
+
'performer.name': result.performer.name,
|
|
120
|
+
'performer.email': result.performer.email
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
span.recordException(error);
|
|
127
|
+
span.setStatus({ code: 2, message: error.message }); // ERROR
|
|
128
|
+
throw error;
|
|
129
|
+
} finally {
|
|
130
|
+
span.end();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Extract trace context from request headers (for connecting client and server spans)
|
|
136
|
+
export function extractTraceContext(request) {
|
|
137
|
+
const headers = {};
|
|
138
|
+
if (request?.headers) {
|
|
139
|
+
// Convert Headers object to plain object
|
|
140
|
+
for (const [key, value] of request.headers.entries()) {
|
|
141
|
+
headers[key] = value;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return propagation.extract(context.active(), headers);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Inject trace context into response headers (for client-side correlation)
|
|
148
|
+
export function injectTraceContext(response, span) {
|
|
149
|
+
const headers = {};
|
|
150
|
+
propagation.inject(context.active(), headers);
|
|
151
|
+
|
|
152
|
+
// Add trace context headers to response
|
|
153
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
154
|
+
response.headers?.set(key, value);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Add trace ID for client correlation
|
|
158
|
+
const spanContext = span?.spanContext();
|
|
159
|
+
if (spanContext) {
|
|
160
|
+
response.headers?.set('X-Trace-Id', spanContext.traceId);
|
|
161
|
+
response.headers?.set('X-Span-Id', spanContext.spanId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return response;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Shutdown server telemetry
|
|
168
|
+
export async function shutdownServerTelemetry() {
|
|
169
|
+
if (sdk) {
|
|
170
|
+
try {
|
|
171
|
+
await sdk.shutdown();
|
|
172
|
+
console.log('[Server Telemetry] Shutdown complete');
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error('[Server Telemetry] Shutdown error:', error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// SvelteKit-specific helpers for performers portal
|
|
180
|
+
export const performersPortalTelemetry = {
|
|
181
|
+
// Wrap page load functions
|
|
182
|
+
wrapLoad: (loadFn, context) => trackPerformerSSRDataLoad(loadFn, context),
|
|
183
|
+
|
|
184
|
+
// Wrap performer actions
|
|
185
|
+
wrapAction: (actionFn, actionName) => {
|
|
186
|
+
return async (event) => {
|
|
187
|
+
const span = createSSRSpan(`ssr.performer.action.${actionName}`, event.request, {
|
|
188
|
+
'ssr.context': 'action',
|
|
189
|
+
'action.name': actionName,
|
|
190
|
+
'performer.context': true
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const result = await actionFn(event);
|
|
195
|
+
span.setStatus({ code: 1 }); // OK
|
|
196
|
+
return result;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
span.recordException(error);
|
|
199
|
+
span.setStatus({ code: 2, message: error.message }); // ERROR
|
|
200
|
+
throw error;
|
|
201
|
+
} finally {
|
|
202
|
+
span.end();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Auto-initialize if in server environment
|
|
209
|
+
if (typeof window === 'undefined' && typeof global !== 'undefined') {
|
|
210
|
+
// Server environment
|
|
211
|
+
initServerTelemetry();
|
|
212
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.server.spec.d.ts","sourceRoot":"","sources":["../src/lib/telemetry.server.spec.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock all OpenTelemetry modules
|
|
4
|
+
vi.mock('@opentelemetry/instrumentation', () => ({
|
|
5
|
+
registerInstrumentations: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('@opentelemetry/sdk-node', () => ({
|
|
9
|
+
NodeSDK: vi.fn(() => ({
|
|
10
|
+
start: vi.fn(),
|
|
11
|
+
shutdown: vi.fn(() => Promise.resolve()),
|
|
12
|
+
})),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock('@opentelemetry/exporter-trace-otlp-http', () => ({
|
|
16
|
+
OTLPTraceExporter: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('@opentelemetry/resources', () => ({
|
|
20
|
+
Resource: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('@opentelemetry/semantic-conventions', () => ({
|
|
24
|
+
SEMRESATTRS_SERVICE_NAME: 'service.name',
|
|
25
|
+
SEMRESATTRS_SERVICE_VERSION: 'service.version',
|
|
26
|
+
SEMRESATTRS_DEPLOYMENT_ENVIRONMENT: 'deployment.environment',
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('@opentelemetry/instrumentation-http', () => ({
|
|
30
|
+
HttpInstrumentation: vi.fn(() => ({})),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock('@opentelemetry/instrumentation-fetch', () => ({
|
|
34
|
+
FetchInstrumentation: vi.fn(() => ({})),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock('@opentelemetry/auto-instrumentations-node', () => ({
|
|
38
|
+
getNodeAutoInstrumentations: vi.fn(() => []),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const mockSpan = {
|
|
42
|
+
setAttribute: vi.fn(),
|
|
43
|
+
setAttributes: vi.fn(),
|
|
44
|
+
setStatus: vi.fn(),
|
|
45
|
+
recordException: vi.fn(),
|
|
46
|
+
end: vi.fn(),
|
|
47
|
+
spanContext: vi.fn(() => ({
|
|
48
|
+
traceId: 'test-trace-id',
|
|
49
|
+
spanId: 'test-span-id',
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const mockTracer = {
|
|
54
|
+
startSpan: vi.fn(() => mockSpan),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const mockContext = {
|
|
58
|
+
active: vi.fn(() => ({})),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const mockPropagation = {
|
|
62
|
+
extract: vi.fn(() => ({})),
|
|
63
|
+
inject: vi.fn(),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
vi.mock('@opentelemetry/api', () => ({
|
|
67
|
+
trace: {
|
|
68
|
+
getTracer: vi.fn(() => mockTracer),
|
|
69
|
+
},
|
|
70
|
+
context: mockContext,
|
|
71
|
+
propagation: mockPropagation,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
describe('telemetry.server.js', () => {
|
|
75
|
+
let telemetryServer;
|
|
76
|
+
|
|
77
|
+
beforeEach(async () => {
|
|
78
|
+
vi.resetModules();
|
|
79
|
+
vi.clearAllMocks();
|
|
80
|
+
|
|
81
|
+
// Ensure we're in a server environment
|
|
82
|
+
delete global.window;
|
|
83
|
+
|
|
84
|
+
// Stub environment variables
|
|
85
|
+
vi.stubEnv('VITE_OTEL_TRACES_ENABLED', 'true');
|
|
86
|
+
vi.stubEnv('VITE_OTEL_SERVICE_NAME', 'test-service');
|
|
87
|
+
vi.stubEnv('VITE_ENVIRONMENT', 'test');
|
|
88
|
+
|
|
89
|
+
telemetryServer = await import('./telemetry.server.js');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
vi.unstubAllEnvs();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('initServerTelemetry', () => {
|
|
97
|
+
it('initializes server telemetry when enabled', async () => {
|
|
98
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
99
|
+
|
|
100
|
+
// Module auto-initializes, calling initServerTelemetry will re-init
|
|
101
|
+
telemetryServer.initServerTelemetry();
|
|
102
|
+
|
|
103
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('logs disabled message when tracing is disabled', async () => {
|
|
107
|
+
vi.resetModules();
|
|
108
|
+
vi.stubEnv('VITE_OTEL_TRACES_ENABLED', 'false');
|
|
109
|
+
|
|
110
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
111
|
+
|
|
112
|
+
telemetryServer = await import('./telemetry.server.js');
|
|
113
|
+
telemetryServer.initServerTelemetry();
|
|
114
|
+
|
|
115
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
116
|
+
'[Server Telemetry] Tracing is disabled'
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('createSSRSpan', () => {
|
|
122
|
+
it('creates SSR span with correct name and attributes', () => {
|
|
123
|
+
const mockRequest = {
|
|
124
|
+
method: 'GET',
|
|
125
|
+
url: 'http://localhost:3000/test',
|
|
126
|
+
headers: {
|
|
127
|
+
'user-agent': 'test-agent',
|
|
128
|
+
referer: 'http://localhost:3000/',
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const span = telemetryServer.createSSRSpan(
|
|
133
|
+
'ssr.test',
|
|
134
|
+
mockRequest,
|
|
135
|
+
{ custom: 'attribute' }
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(mockTracer.startSpan).toHaveBeenCalledWith('ssr.test', {
|
|
139
|
+
attributes: {
|
|
140
|
+
'span.kind': 'server',
|
|
141
|
+
'ssr.type': 'page_load',
|
|
142
|
+
'portal.type': 'performers',
|
|
143
|
+
'http.method': 'GET',
|
|
144
|
+
'http.url': 'http://localhost:3000/test',
|
|
145
|
+
'http.user_agent': 'test-agent',
|
|
146
|
+
'http.referer': 'http://localhost:3000/',
|
|
147
|
+
custom: 'attribute',
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(span).toBeDefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('handles null request gracefully', () => {
|
|
155
|
+
const span = telemetryServer.createSSRSpan('ssr.null-request', null);
|
|
156
|
+
|
|
157
|
+
expect(mockTracer.startSpan).toHaveBeenCalledWith('ssr.null-request', {
|
|
158
|
+
attributes: {
|
|
159
|
+
'span.kind': 'server',
|
|
160
|
+
'ssr.type': 'page_load',
|
|
161
|
+
'portal.type': 'performers',
|
|
162
|
+
'http.method': 'GET',
|
|
163
|
+
'http.url': undefined,
|
|
164
|
+
'http.user_agent': undefined,
|
|
165
|
+
'http.referer': undefined,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(span).toBeDefined();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('trackPerformerSSRDataLoad', () => {
|
|
174
|
+
it('wraps load function and tracks successful execution', async () => {
|
|
175
|
+
const mockLoadFn = vi.fn().mockResolvedValue({
|
|
176
|
+
performer: { id: 1, name: 'Test Performer', email: 'test@test.com' },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const mockEvent = {
|
|
180
|
+
request: {
|
|
181
|
+
method: 'GET',
|
|
182
|
+
url: 'http://localhost:3000/performer/1',
|
|
183
|
+
headers: {},
|
|
184
|
+
},
|
|
185
|
+
route: { id: '/performer/[id]' },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const wrappedFn = telemetryServer.trackPerformerSSRDataLoad(
|
|
189
|
+
mockLoadFn,
|
|
190
|
+
'performer-load'
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const result = await wrappedFn(mockEvent);
|
|
194
|
+
|
|
195
|
+
expect(mockLoadFn).toHaveBeenCalledWith(mockEvent);
|
|
196
|
+
expect(result.performer.id).toBe(1);
|
|
197
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: 1 });
|
|
198
|
+
expect(mockSpan.setAttributes).toHaveBeenCalledWith({
|
|
199
|
+
'performer.id': 1,
|
|
200
|
+
'performer.name': 'Test Performer',
|
|
201
|
+
'performer.email': 'test@test.com',
|
|
202
|
+
});
|
|
203
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('tracks errors and re-throws them', async () => {
|
|
207
|
+
const testError = new Error('Load failed');
|
|
208
|
+
const mockLoadFn = vi.fn().mockRejectedValue(testError);
|
|
209
|
+
|
|
210
|
+
const mockEvent = {
|
|
211
|
+
request: {
|
|
212
|
+
method: 'GET',
|
|
213
|
+
url: 'http://localhost:3000/test',
|
|
214
|
+
headers: {},
|
|
215
|
+
},
|
|
216
|
+
route: { id: '/test' },
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const wrappedFn = telemetryServer.trackPerformerSSRDataLoad(
|
|
220
|
+
mockLoadFn,
|
|
221
|
+
'test-load'
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
await expect(wrappedFn(mockEvent)).rejects.toThrow('Load failed');
|
|
225
|
+
|
|
226
|
+
expect(mockSpan.recordException).toHaveBeenCalledWith(testError);
|
|
227
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
228
|
+
code: 2,
|
|
229
|
+
message: 'Load failed',
|
|
230
|
+
});
|
|
231
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('handles results without performer data', async () => {
|
|
235
|
+
const mockLoadFn = vi.fn().mockResolvedValue({ data: 'test' });
|
|
236
|
+
|
|
237
|
+
const mockEvent = {
|
|
238
|
+
request: {
|
|
239
|
+
method: 'GET',
|
|
240
|
+
url: 'http://localhost:3000/test',
|
|
241
|
+
headers: {},
|
|
242
|
+
},
|
|
243
|
+
route: { id: '/test' },
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const wrappedFn = telemetryServer.trackPerformerSSRDataLoad(
|
|
247
|
+
mockLoadFn,
|
|
248
|
+
'test-load'
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const result = await wrappedFn(mockEvent);
|
|
252
|
+
|
|
253
|
+
expect(result.data).toBe('test');
|
|
254
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: 1 });
|
|
255
|
+
// setAttributes should not be called for performer data
|
|
256
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('extractTraceContext', () => {
|
|
261
|
+
it('extracts trace context from request headers', () => {
|
|
262
|
+
const mockRequest = {
|
|
263
|
+
headers: new Map([
|
|
264
|
+
['x-b3-traceid', 'test-trace'],
|
|
265
|
+
['x-b3-spanid', 'test-span'],
|
|
266
|
+
]),
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
mockRequest.headers.entries = function () {
|
|
270
|
+
return this[Symbol.iterator]();
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
telemetryServer.extractTraceContext(mockRequest);
|
|
274
|
+
|
|
275
|
+
expect(mockPropagation.extract).toHaveBeenCalled();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('handles null request', () => {
|
|
279
|
+
const result = telemetryServer.extractTraceContext(null);
|
|
280
|
+
|
|
281
|
+
expect(mockPropagation.extract).toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('handles request without headers', () => {
|
|
285
|
+
const mockRequest = {};
|
|
286
|
+
|
|
287
|
+
const result = telemetryServer.extractTraceContext(mockRequest);
|
|
288
|
+
|
|
289
|
+
expect(mockPropagation.extract).toHaveBeenCalled();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('injectTraceContext', () => {
|
|
294
|
+
it('injects trace context into response headers', () => {
|
|
295
|
+
const mockResponse = {
|
|
296
|
+
headers: {
|
|
297
|
+
set: vi.fn(),
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
mockPropagation.inject.mockImplementation((ctx, headers) => {
|
|
302
|
+
headers['x-b3-traceid'] = 'injected-trace';
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
telemetryServer.injectTraceContext(mockResponse, mockSpan);
|
|
306
|
+
|
|
307
|
+
expect(mockPropagation.inject).toHaveBeenCalled();
|
|
308
|
+
expect(mockResponse.headers.set).toHaveBeenCalledWith(
|
|
309
|
+
'X-Trace-Id',
|
|
310
|
+
'test-trace-id'
|
|
311
|
+
);
|
|
312
|
+
expect(mockResponse.headers.set).toHaveBeenCalledWith(
|
|
313
|
+
'X-Span-Id',
|
|
314
|
+
'test-span-id'
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('handles null span context', () => {
|
|
319
|
+
const mockResponse = {
|
|
320
|
+
headers: {
|
|
321
|
+
set: vi.fn(),
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const nullContextSpan = {
|
|
326
|
+
spanContext: vi.fn(() => null),
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Should not throw
|
|
330
|
+
expect(() =>
|
|
331
|
+
telemetryServer.injectTraceContext(mockResponse, nullContextSpan)
|
|
332
|
+
).not.toThrow();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('handles response without headers', () => {
|
|
336
|
+
const mockResponse = {};
|
|
337
|
+
|
|
338
|
+
// Should not throw
|
|
339
|
+
expect(() =>
|
|
340
|
+
telemetryServer.injectTraceContext(mockResponse, mockSpan)
|
|
341
|
+
).not.toThrow();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('shutdownServerTelemetry', () => {
|
|
346
|
+
it('shuts down SDK gracefully', async () => {
|
|
347
|
+
const consoleSpy = vi.spyOn(console, 'log');
|
|
348
|
+
|
|
349
|
+
await telemetryServer.shutdownServerTelemetry();
|
|
350
|
+
|
|
351
|
+
// May or may not log depending on SDK state
|
|
352
|
+
expect(true).toBe(true);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('performersPortalTelemetry', () => {
|
|
357
|
+
it('exports wrapLoad helper', () => {
|
|
358
|
+
expect(telemetryServer.performersPortalTelemetry.wrapLoad).toBeDefined();
|
|
359
|
+
expect(
|
|
360
|
+
typeof telemetryServer.performersPortalTelemetry.wrapLoad
|
|
361
|
+
).toBe('function');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('exports wrapAction helper', () => {
|
|
365
|
+
expect(telemetryServer.performersPortalTelemetry.wrapAction).toBeDefined();
|
|
366
|
+
expect(
|
|
367
|
+
typeof telemetryServer.performersPortalTelemetry.wrapAction
|
|
368
|
+
).toBe('function');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('wrapLoad returns wrapped function', () => {
|
|
372
|
+
const mockFn = vi.fn();
|
|
373
|
+
const wrapped = telemetryServer.performersPortalTelemetry.wrapLoad(
|
|
374
|
+
mockFn,
|
|
375
|
+
'test'
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
expect(typeof wrapped).toBe('function');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('wrapAction handles successful action', async () => {
|
|
382
|
+
const mockActionFn = vi.fn().mockResolvedValue({ success: true });
|
|
383
|
+
|
|
384
|
+
const mockEvent = {
|
|
385
|
+
request: {
|
|
386
|
+
method: 'POST',
|
|
387
|
+
url: 'http://localhost:3000/action',
|
|
388
|
+
headers: {},
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const wrappedAction =
|
|
393
|
+
telemetryServer.performersPortalTelemetry.wrapAction(
|
|
394
|
+
mockActionFn,
|
|
395
|
+
'test-action'
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const result = await wrappedAction(mockEvent);
|
|
399
|
+
|
|
400
|
+
expect(mockActionFn).toHaveBeenCalledWith(mockEvent);
|
|
401
|
+
expect(result.success).toBe(true);
|
|
402
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: 1 });
|
|
403
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('wrapAction handles action errors', async () => {
|
|
407
|
+
const testError = new Error('Action failed');
|
|
408
|
+
const mockActionFn = vi.fn().mockRejectedValue(testError);
|
|
409
|
+
|
|
410
|
+
const mockEvent = {
|
|
411
|
+
request: {
|
|
412
|
+
method: 'POST',
|
|
413
|
+
url: 'http://localhost:3000/action',
|
|
414
|
+
headers: {},
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const wrappedAction =
|
|
419
|
+
telemetryServer.performersPortalTelemetry.wrapAction(
|
|
420
|
+
mockActionFn,
|
|
421
|
+
'failing-action'
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
await expect(wrappedAction(mockEvent)).rejects.toThrow('Action failed');
|
|
425
|
+
|
|
426
|
+
expect(mockSpan.recordException).toHaveBeenCalledWith(testError);
|
|
427
|
+
expect(mockSpan.setStatus).toHaveBeenCalledWith({
|
|
428
|
+
code: 2,
|
|
429
|
+
message: 'Action failed',
|
|
430
|
+
});
|
|
431
|
+
expect(mockSpan.end).toHaveBeenCalled();
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.spec.d.ts","sourceRoot":"","sources":["../src/lib/telemetry.spec.js"],"names":[],"mappings":""}
|