@syncular/observability-sentry 0.0.1-100
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/dist/browser.d.ts +35 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +155 -0
- package/dist/browser.js.map +1 -0
- package/dist/cloudflare.d.ts +47 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +163 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/shared.d.ts +31 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +147 -0
- package/dist/shared.js.map +1 -0
- package/package.json +55 -0
- package/src/browser.ts +215 -0
- package/src/cloudflare.test.ts +49 -0
- package/src/cloudflare.ts +230 -0
- package/src/index.ts +3 -0
- package/src/shared.test.ts +145 -0
- package/src/shared.ts +200 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@syncular/observability-sentry",
|
|
3
|
+
"version": "0.0.1-100",
|
|
4
|
+
"description": "Sentry adapters for Syncular telemetry",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Benjamin Kniffler",
|
|
7
|
+
"homepage": "https://syncular.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/syncular/syncular.git",
|
|
11
|
+
"directory": "packages/observability-sentry"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/syncular/syncular/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"sync",
|
|
18
|
+
"offline-first",
|
|
19
|
+
"observability",
|
|
20
|
+
"sentry",
|
|
21
|
+
"telemetry"
|
|
22
|
+
],
|
|
23
|
+
"private": false,
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"bun": "./src/index.ts",
|
|
31
|
+
"import": {
|
|
32
|
+
"types": "./src/index.ts",
|
|
33
|
+
"default": "./src/index.ts"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "bun test src",
|
|
39
|
+
"tsgo": "tsgo --noEmit",
|
|
40
|
+
"build": "tsgo",
|
|
41
|
+
"release": "bunx syncular-publish"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@sentry/cloudflare": "^10.38.0",
|
|
45
|
+
"@sentry/react": "^10.38.0",
|
|
46
|
+
"@syncular/core": "0.0.1"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@syncular/config": "0.0.0"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist",
|
|
53
|
+
"src"
|
|
54
|
+
]
|
|
55
|
+
}
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/react';
|
|
2
|
+
import {
|
|
3
|
+
configureSyncTelemetry,
|
|
4
|
+
type SyncMetricOptions,
|
|
5
|
+
type SyncTelemetry,
|
|
6
|
+
type SyncTelemetryAttributeValue,
|
|
7
|
+
} from '@syncular/core';
|
|
8
|
+
import { createSentrySyncTelemetry } from './shared';
|
|
9
|
+
|
|
10
|
+
export type BrowserSentryInitOptions = Parameters<typeof Sentry.init>[0];
|
|
11
|
+
export type BrowserSentryCaptureMessageLevel = Parameters<
|
|
12
|
+
typeof Sentry.captureMessage
|
|
13
|
+
>[1];
|
|
14
|
+
|
|
15
|
+
interface BrowserSentryCaptureMessageOptions {
|
|
16
|
+
level?: BrowserSentryCaptureMessageLevel;
|
|
17
|
+
tags?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type BrowserSentryLogLevel =
|
|
21
|
+
| 'trace'
|
|
22
|
+
| 'debug'
|
|
23
|
+
| 'info'
|
|
24
|
+
| 'warn'
|
|
25
|
+
| 'error'
|
|
26
|
+
| 'fatal';
|
|
27
|
+
|
|
28
|
+
interface BrowserSentryLogOptions {
|
|
29
|
+
level?: BrowserSentryLogLevel;
|
|
30
|
+
attributes?: Record<string, SyncTelemetryAttributeValue>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveBrowserLogMethod(
|
|
34
|
+
level: BrowserSentryLogLevel
|
|
35
|
+
):
|
|
36
|
+
| ((
|
|
37
|
+
message: string,
|
|
38
|
+
attributes?: Record<string, SyncTelemetryAttributeValue>
|
|
39
|
+
) => void)
|
|
40
|
+
| null {
|
|
41
|
+
switch (level) {
|
|
42
|
+
case 'trace':
|
|
43
|
+
return Sentry.logger.trace ?? Sentry.logger.debug ?? Sentry.logger.info;
|
|
44
|
+
case 'debug':
|
|
45
|
+
return Sentry.logger.debug ?? Sentry.logger.info;
|
|
46
|
+
case 'info':
|
|
47
|
+
return Sentry.logger.info;
|
|
48
|
+
case 'warn':
|
|
49
|
+
return Sentry.logger.warn ?? Sentry.logger.info;
|
|
50
|
+
case 'error':
|
|
51
|
+
return Sentry.logger.error ?? Sentry.logger.warn ?? Sentry.logger.info;
|
|
52
|
+
case 'fatal':
|
|
53
|
+
return (
|
|
54
|
+
Sentry.logger.fatal ??
|
|
55
|
+
Sentry.logger.error ??
|
|
56
|
+
Sentry.logger.warn ??
|
|
57
|
+
Sentry.logger.info
|
|
58
|
+
);
|
|
59
|
+
default:
|
|
60
|
+
return Sentry.logger.info;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toCountMetricOptions(
|
|
65
|
+
options?: SyncMetricOptions
|
|
66
|
+
): Parameters<typeof Sentry.metrics.count>[2] | undefined {
|
|
67
|
+
if (!options?.attributes) return undefined;
|
|
68
|
+
return { attributes: options.attributes };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function toValueMetricOptions(
|
|
72
|
+
options?: SyncMetricOptions
|
|
73
|
+
): Parameters<typeof Sentry.metrics.gauge>[2] | undefined {
|
|
74
|
+
if (!options) return undefined;
|
|
75
|
+
const hasAttributes = Boolean(options.attributes);
|
|
76
|
+
const hasUnit = Boolean(options.unit);
|
|
77
|
+
if (!hasAttributes && !hasUnit) return undefined;
|
|
78
|
+
return {
|
|
79
|
+
attributes: options.attributes,
|
|
80
|
+
unit: options.unit,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a Syncular telemetry backend wired to `@sentry/react`.
|
|
86
|
+
*/
|
|
87
|
+
export function createBrowserSentryTelemetry(): SyncTelemetry {
|
|
88
|
+
return createSentrySyncTelemetry({
|
|
89
|
+
logger: Sentry.logger,
|
|
90
|
+
startSpan(options, callback) {
|
|
91
|
+
return Sentry.startSpan(options, (span) =>
|
|
92
|
+
callback({
|
|
93
|
+
setAttribute(name, value) {
|
|
94
|
+
span.setAttribute(name, value);
|
|
95
|
+
},
|
|
96
|
+
setAttributes(attributes) {
|
|
97
|
+
span.setAttributes(attributes);
|
|
98
|
+
},
|
|
99
|
+
setStatus(status) {
|
|
100
|
+
span.setStatus({
|
|
101
|
+
code: status === 'ok' ? 1 : 2,
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
metrics: {
|
|
108
|
+
count(name, value, options) {
|
|
109
|
+
const metricOptions = toCountMetricOptions(options);
|
|
110
|
+
if (metricOptions) {
|
|
111
|
+
Sentry.metrics.count(name, value, metricOptions);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
Sentry.metrics.count(name, value);
|
|
115
|
+
},
|
|
116
|
+
gauge(name, value, options) {
|
|
117
|
+
const metricOptions = toValueMetricOptions(options);
|
|
118
|
+
if (metricOptions) {
|
|
119
|
+
Sentry.metrics.gauge(name, value, metricOptions);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
Sentry.metrics.gauge(name, value);
|
|
123
|
+
},
|
|
124
|
+
distribution(name, value, options) {
|
|
125
|
+
const metricOptions = toValueMetricOptions(options);
|
|
126
|
+
if (metricOptions) {
|
|
127
|
+
Sentry.metrics.distribution(name, value, metricOptions);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
Sentry.metrics.distribution(name, value);
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
captureException(error) {
|
|
134
|
+
Sentry.captureException(error);
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Configure Syncular core telemetry to use the browser Sentry adapter.
|
|
141
|
+
*/
|
|
142
|
+
export function configureBrowserSentryTelemetry(): SyncTelemetry {
|
|
143
|
+
const telemetry = createBrowserSentryTelemetry();
|
|
144
|
+
configureSyncTelemetry(telemetry);
|
|
145
|
+
return telemetry;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Initialize browser Sentry and configure Syncular telemetry.
|
|
150
|
+
*/
|
|
151
|
+
export function initAndConfigureBrowserSentry(
|
|
152
|
+
options: BrowserSentryInitOptions
|
|
153
|
+
): SyncTelemetry {
|
|
154
|
+
const configuredOptions = ensureBrowserTracingIntegration(options);
|
|
155
|
+
Sentry.init(configuredOptions);
|
|
156
|
+
return configureBrowserSentryTelemetry();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ensureBrowserTracingIntegration(
|
|
160
|
+
options: BrowserSentryInitOptions
|
|
161
|
+
): BrowserSentryInitOptions {
|
|
162
|
+
const integrations = options.integrations;
|
|
163
|
+
if (typeof integrations === 'function') return options;
|
|
164
|
+
|
|
165
|
+
const configuredIntegrations = integrations ?? [];
|
|
166
|
+
const hasBrowserTracing = configuredIntegrations.some(
|
|
167
|
+
(integration) => integration.name === 'BrowserTracing'
|
|
168
|
+
);
|
|
169
|
+
if (hasBrowserTracing) return options;
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...options,
|
|
173
|
+
integrations: [
|
|
174
|
+
Sentry.browserTracingIntegration(),
|
|
175
|
+
...configuredIntegrations,
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Capture a browser message in Sentry with optional tags.
|
|
182
|
+
*/
|
|
183
|
+
export function captureBrowserSentryMessage(
|
|
184
|
+
message: string,
|
|
185
|
+
options?: BrowserSentryCaptureMessageOptions
|
|
186
|
+
): void {
|
|
187
|
+
if (!options?.tags || Object.keys(options.tags).length === 0) {
|
|
188
|
+
Sentry.captureMessage(message, options?.level);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
Sentry.withScope((scope) => {
|
|
193
|
+
for (const [name, value] of Object.entries(options.tags ?? {})) {
|
|
194
|
+
scope.setTag(name, value);
|
|
195
|
+
}
|
|
196
|
+
Sentry.captureMessage(message, options?.level);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Emit a browser Sentry log entry.
|
|
202
|
+
*/
|
|
203
|
+
export function logBrowserSentryMessage(
|
|
204
|
+
message: string,
|
|
205
|
+
options?: BrowserSentryLogOptions
|
|
206
|
+
): void {
|
|
207
|
+
const level = options?.level ?? 'info';
|
|
208
|
+
const logMethod = resolveBrowserLogMethod(level);
|
|
209
|
+
if (!logMethod) return;
|
|
210
|
+
if (!options?.attributes || Object.keys(options.attributes).length === 0) {
|
|
211
|
+
logMethod(message);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
logMethod(message, options.attributes);
|
|
215
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
attachCloudflareSentryTraceHeaders,
|
|
4
|
+
getCloudflareSentryTraceHeaders,
|
|
5
|
+
} from './cloudflare';
|
|
6
|
+
|
|
7
|
+
describe('cloudflare tracing helpers', () => {
|
|
8
|
+
test('extracts trace headers from sentry trace data', () => {
|
|
9
|
+
expect(
|
|
10
|
+
getCloudflareSentryTraceHeaders({
|
|
11
|
+
'sentry-trace': 'trace-id-span-id-1',
|
|
12
|
+
baggage: 'sample_rate=1',
|
|
13
|
+
})
|
|
14
|
+
).toEqual({
|
|
15
|
+
sentryTrace: 'trace-id-span-id-1',
|
|
16
|
+
baggage: 'sample_rate=1',
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('returns original request when trace headers are missing', () => {
|
|
21
|
+
const request = new Request('https://example.com/api/sync', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'x-custom': '1' },
|
|
24
|
+
body: '{}',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const result = attachCloudflareSentryTraceHeaders(request, {});
|
|
28
|
+
expect(result).toBe(request);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('adds sentry trace headers to cloned request', async () => {
|
|
32
|
+
const request = new Request('https://example.com/api/sync', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'x-custom': '1' },
|
|
35
|
+
body: '{"ok":true}',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const result = attachCloudflareSentryTraceHeaders(request, {
|
|
39
|
+
sentryTrace: 'trace-id-span-id-1',
|
|
40
|
+
baggage: 'sample_rate=1',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result).not.toBe(request);
|
|
44
|
+
expect(result.headers.get('x-custom')).toBe('1');
|
|
45
|
+
expect(result.headers.get('sentry-trace')).toBe('trace-id-span-id-1');
|
|
46
|
+
expect(result.headers.get('baggage')).toBe('sample_rate=1');
|
|
47
|
+
expect(await result.text()).toBe('{"ok":true}');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/cloudflare';
|
|
2
|
+
import {
|
|
3
|
+
configureSyncTelemetry,
|
|
4
|
+
type SyncMetricOptions,
|
|
5
|
+
type SyncTelemetry,
|
|
6
|
+
type SyncTelemetryAttributeValue,
|
|
7
|
+
} from '@syncular/core';
|
|
8
|
+
import { createSentrySyncTelemetry } from './shared';
|
|
9
|
+
|
|
10
|
+
function toCountMetricOptions(
|
|
11
|
+
options?: SyncMetricOptions
|
|
12
|
+
): Parameters<typeof Sentry.metrics.count>[2] | undefined {
|
|
13
|
+
if (!options?.attributes) return undefined;
|
|
14
|
+
return { attributes: options.attributes };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toValueMetricOptions(
|
|
18
|
+
options?: SyncMetricOptions
|
|
19
|
+
): Parameters<typeof Sentry.metrics.gauge>[2] | undefined {
|
|
20
|
+
if (!options) return undefined;
|
|
21
|
+
const hasAttributes = Boolean(options.attributes);
|
|
22
|
+
const hasUnit = Boolean(options.unit);
|
|
23
|
+
if (!hasAttributes && !hasUnit) return undefined;
|
|
24
|
+
return {
|
|
25
|
+
attributes: options.attributes,
|
|
26
|
+
unit: options.unit,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type CloudflareSentryCaptureMessageLevel = Parameters<
|
|
31
|
+
typeof Sentry.captureMessage
|
|
32
|
+
>[1];
|
|
33
|
+
|
|
34
|
+
interface CloudflareSentryCaptureMessageOptions {
|
|
35
|
+
level?: CloudflareSentryCaptureMessageLevel;
|
|
36
|
+
tags?: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type CloudflareSentryLogLevel =
|
|
40
|
+
| 'trace'
|
|
41
|
+
| 'debug'
|
|
42
|
+
| 'info'
|
|
43
|
+
| 'warn'
|
|
44
|
+
| 'error'
|
|
45
|
+
| 'fatal';
|
|
46
|
+
|
|
47
|
+
interface CloudflareSentryLogOptions {
|
|
48
|
+
level?: CloudflareSentryLogLevel;
|
|
49
|
+
attributes?: Record<string, SyncTelemetryAttributeValue>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveCloudflareLogMethod(
|
|
53
|
+
level: CloudflareSentryLogLevel
|
|
54
|
+
):
|
|
55
|
+
| ((
|
|
56
|
+
message: string,
|
|
57
|
+
attributes?: Record<string, SyncTelemetryAttributeValue>
|
|
58
|
+
) => void)
|
|
59
|
+
| null {
|
|
60
|
+
switch (level) {
|
|
61
|
+
case 'trace':
|
|
62
|
+
return Sentry.logger.trace ?? Sentry.logger.debug ?? Sentry.logger.info;
|
|
63
|
+
case 'debug':
|
|
64
|
+
return Sentry.logger.debug ?? Sentry.logger.info;
|
|
65
|
+
case 'info':
|
|
66
|
+
return Sentry.logger.info;
|
|
67
|
+
case 'warn':
|
|
68
|
+
return Sentry.logger.warn ?? Sentry.logger.info;
|
|
69
|
+
case 'error':
|
|
70
|
+
return Sentry.logger.error ?? Sentry.logger.warn ?? Sentry.logger.info;
|
|
71
|
+
case 'fatal':
|
|
72
|
+
return (
|
|
73
|
+
Sentry.logger.fatal ??
|
|
74
|
+
Sentry.logger.error ??
|
|
75
|
+
Sentry.logger.warn ??
|
|
76
|
+
Sentry.logger.info
|
|
77
|
+
);
|
|
78
|
+
default:
|
|
79
|
+
return Sentry.logger.info;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Re-export Cloudflare Sentry worker wrapper.
|
|
85
|
+
*/
|
|
86
|
+
export const withCloudflareSentry = Sentry.withSentry;
|
|
87
|
+
export const instrumentCloudflareDurableObjectWithSentry =
|
|
88
|
+
Sentry.instrumentDurableObjectWithSentry;
|
|
89
|
+
|
|
90
|
+
export interface CloudflareSentryTraceHeaders {
|
|
91
|
+
sentryTrace?: string;
|
|
92
|
+
baggage?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read current trace headers from the active Cloudflare Sentry scope.
|
|
97
|
+
*/
|
|
98
|
+
export function getCloudflareSentryTraceHeaders(
|
|
99
|
+
traceData: ReturnType<typeof Sentry.getTraceData> = Sentry.getTraceData()
|
|
100
|
+
): CloudflareSentryTraceHeaders {
|
|
101
|
+
const sentryTrace = traceData['sentry-trace']?.trim();
|
|
102
|
+
const baggage = traceData.baggage?.trim();
|
|
103
|
+
return {
|
|
104
|
+
...(sentryTrace ? { sentryTrace } : {}),
|
|
105
|
+
...(baggage ? { baggage } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clone a request and attach active Cloudflare trace headers when available.
|
|
111
|
+
*/
|
|
112
|
+
export function attachCloudflareSentryTraceHeaders(
|
|
113
|
+
request: Request,
|
|
114
|
+
traceHeaders: CloudflareSentryTraceHeaders = getCloudflareSentryTraceHeaders()
|
|
115
|
+
): Request {
|
|
116
|
+
if (!traceHeaders.sentryTrace && !traceHeaders.baggage) {
|
|
117
|
+
return request;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const headers = new Headers(request.headers);
|
|
121
|
+
if (traceHeaders.sentryTrace) {
|
|
122
|
+
headers.set('sentry-trace', traceHeaders.sentryTrace);
|
|
123
|
+
}
|
|
124
|
+
if (traceHeaders.baggage) {
|
|
125
|
+
headers.set('baggage', traceHeaders.baggage);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new Request(request, { headers });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a Syncular telemetry backend wired to `@sentry/cloudflare`.
|
|
133
|
+
*/
|
|
134
|
+
export function createCloudflareSentryTelemetry(): SyncTelemetry {
|
|
135
|
+
return createSentrySyncTelemetry({
|
|
136
|
+
logger: Sentry.logger,
|
|
137
|
+
startSpan(options, callback) {
|
|
138
|
+
return Sentry.startSpan(options, (span) =>
|
|
139
|
+
callback({
|
|
140
|
+
setAttribute(name, value) {
|
|
141
|
+
span.setAttribute(name, value);
|
|
142
|
+
},
|
|
143
|
+
setAttributes(attributes) {
|
|
144
|
+
span.setAttributes(attributes);
|
|
145
|
+
},
|
|
146
|
+
setStatus(status) {
|
|
147
|
+
span.setStatus({
|
|
148
|
+
code: status === 'ok' ? 1 : 2,
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
},
|
|
154
|
+
metrics: {
|
|
155
|
+
count(name, value, options) {
|
|
156
|
+
const metricOptions = toCountMetricOptions(options);
|
|
157
|
+
if (metricOptions) {
|
|
158
|
+
Sentry.metrics.count(name, value, metricOptions);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
Sentry.metrics.count(name, value);
|
|
162
|
+
},
|
|
163
|
+
gauge(name, value, options) {
|
|
164
|
+
const metricOptions = toValueMetricOptions(options);
|
|
165
|
+
if (metricOptions) {
|
|
166
|
+
Sentry.metrics.gauge(name, value, metricOptions);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
Sentry.metrics.gauge(name, value);
|
|
170
|
+
},
|
|
171
|
+
distribution(name, value, options) {
|
|
172
|
+
const metricOptions = toValueMetricOptions(options);
|
|
173
|
+
if (metricOptions) {
|
|
174
|
+
Sentry.metrics.distribution(name, value, metricOptions);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
Sentry.metrics.distribution(name, value);
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
captureException(error) {
|
|
181
|
+
Sentry.captureException(error);
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Configure Syncular core telemetry to use the Cloudflare Sentry adapter.
|
|
188
|
+
*/
|
|
189
|
+
export function configureCloudflareSentryTelemetry(): SyncTelemetry {
|
|
190
|
+
const telemetry = createCloudflareSentryTelemetry();
|
|
191
|
+
configureSyncTelemetry(telemetry);
|
|
192
|
+
return telemetry;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Capture a worker message in Sentry with optional tags.
|
|
197
|
+
*/
|
|
198
|
+
export function captureCloudflareSentryMessage(
|
|
199
|
+
message: string,
|
|
200
|
+
options?: CloudflareSentryCaptureMessageOptions
|
|
201
|
+
): void {
|
|
202
|
+
if (!options?.tags || Object.keys(options.tags).length === 0) {
|
|
203
|
+
Sentry.captureMessage(message, options?.level);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
Sentry.withScope((scope) => {
|
|
208
|
+
for (const [name, value] of Object.entries(options.tags ?? {})) {
|
|
209
|
+
scope.setTag(name, value);
|
|
210
|
+
}
|
|
211
|
+
Sentry.captureMessage(message, options?.level);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Emit a Cloudflare Sentry log entry.
|
|
217
|
+
*/
|
|
218
|
+
export function logCloudflareSentryMessage(
|
|
219
|
+
message: string,
|
|
220
|
+
options?: CloudflareSentryLogOptions
|
|
221
|
+
): void {
|
|
222
|
+
const level = options?.level ?? 'info';
|
|
223
|
+
const logMethod = resolveCloudflareLogMethod(level);
|
|
224
|
+
if (!logMethod) return;
|
|
225
|
+
if (!options?.attributes || Object.keys(options.attributes).length === 0) {
|
|
226
|
+
logMethod(message);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
logMethod(message, options.attributes);
|
|
230
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { SyncSpanOptions } from '@syncular/core';
|
|
3
|
+
import {
|
|
4
|
+
createSentrySyncTelemetry,
|
|
5
|
+
type SentryTelemetryAdapter,
|
|
6
|
+
} from './shared';
|
|
7
|
+
|
|
8
|
+
describe('createSentrySyncTelemetry', () => {
|
|
9
|
+
test('routes logs, spans, metrics, and exceptions', () => {
|
|
10
|
+
const logs: Array<{
|
|
11
|
+
level: string;
|
|
12
|
+
message: string;
|
|
13
|
+
attributes?: Record<string, unknown>;
|
|
14
|
+
}> = [];
|
|
15
|
+
const spans: SyncSpanOptions[] = [];
|
|
16
|
+
const metricCalls: Array<{ type: string; name: string; value: number }> =
|
|
17
|
+
[];
|
|
18
|
+
const exceptions: unknown[] = [];
|
|
19
|
+
|
|
20
|
+
const adapter: SentryTelemetryAdapter = {
|
|
21
|
+
logger: {
|
|
22
|
+
info(message, attributes) {
|
|
23
|
+
logs.push({ level: 'info', message, attributes });
|
|
24
|
+
},
|
|
25
|
+
error(message, attributes) {
|
|
26
|
+
logs.push({ level: 'error', message, attributes });
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
startSpan(options, callback) {
|
|
30
|
+
spans.push(options);
|
|
31
|
+
return callback({
|
|
32
|
+
setAttribute() {},
|
|
33
|
+
setAttributes() {},
|
|
34
|
+
setStatus() {},
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
metrics: {
|
|
38
|
+
count(name, value) {
|
|
39
|
+
metricCalls.push({ type: 'count', name, value });
|
|
40
|
+
},
|
|
41
|
+
gauge(name, value) {
|
|
42
|
+
metricCalls.push({ type: 'gauge', name, value });
|
|
43
|
+
},
|
|
44
|
+
distribution(name, value) {
|
|
45
|
+
metricCalls.push({ type: 'distribution', name, value });
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
captureException(error) {
|
|
49
|
+
exceptions.push(error);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const telemetry = createSentrySyncTelemetry(adapter);
|
|
54
|
+
|
|
55
|
+
telemetry.log({ event: 'sync.ok', rowCount: 2 });
|
|
56
|
+
telemetry.log({ event: 'sync.fail', error: 'boom' });
|
|
57
|
+
|
|
58
|
+
const spanValue = telemetry.tracer.startSpan(
|
|
59
|
+
{
|
|
60
|
+
name: 'sync.span',
|
|
61
|
+
op: 'sync',
|
|
62
|
+
},
|
|
63
|
+
(span) => {
|
|
64
|
+
span.setAttribute('transport', 'ws');
|
|
65
|
+
span.setStatus('ok');
|
|
66
|
+
return 123;
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
telemetry.metrics.count('sync.count');
|
|
71
|
+
telemetry.metrics.gauge('sync.gauge', 7);
|
|
72
|
+
telemetry.metrics.distribution('sync.dist', 42);
|
|
73
|
+
telemetry.captureException(new Error('crash'), { requestId: 'r1' });
|
|
74
|
+
|
|
75
|
+
expect(spanValue).toBe(123);
|
|
76
|
+
expect(logs[0]).toEqual({
|
|
77
|
+
level: 'info',
|
|
78
|
+
message: 'sync.ok',
|
|
79
|
+
attributes: { rowCount: 2 },
|
|
80
|
+
});
|
|
81
|
+
expect(logs[1]).toEqual({
|
|
82
|
+
level: 'error',
|
|
83
|
+
message: 'sync.fail',
|
|
84
|
+
attributes: { error: 'boom' },
|
|
85
|
+
});
|
|
86
|
+
expect(spans).toEqual([
|
|
87
|
+
{
|
|
88
|
+
name: 'sync.span',
|
|
89
|
+
op: 'sync',
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
expect(metricCalls).toEqual([
|
|
93
|
+
{ type: 'count', name: 'sync.count', value: 1 },
|
|
94
|
+
{ type: 'gauge', name: 'sync.gauge', value: 7 },
|
|
95
|
+
{ type: 'distribution', name: 'sync.dist', value: 42 },
|
|
96
|
+
]);
|
|
97
|
+
expect(exceptions).toHaveLength(1);
|
|
98
|
+
expect(logs.at(-1)).toEqual({
|
|
99
|
+
level: 'error',
|
|
100
|
+
message: 'sync.exception.context',
|
|
101
|
+
attributes: { requestId: 'r1' },
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('sanitizes non-primitive log attributes', () => {
|
|
106
|
+
const logs: Array<{
|
|
107
|
+
level: string;
|
|
108
|
+
message: string;
|
|
109
|
+
attributes?: Record<string, unknown>;
|
|
110
|
+
}> = [];
|
|
111
|
+
|
|
112
|
+
const telemetry = createSentrySyncTelemetry({
|
|
113
|
+
logger: {
|
|
114
|
+
info(message, attributes) {
|
|
115
|
+
logs.push({ level: 'info', message, attributes });
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
telemetry.log({
|
|
121
|
+
event: 'sync.attributes',
|
|
122
|
+
id: 'abc',
|
|
123
|
+
nested: { ok: true },
|
|
124
|
+
values: [1, 2, 3],
|
|
125
|
+
enabled: true,
|
|
126
|
+
count: 3,
|
|
127
|
+
ignored: undefined,
|
|
128
|
+
nonFinite: Number.POSITIVE_INFINITY,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(logs).toEqual([
|
|
132
|
+
{
|
|
133
|
+
level: 'info',
|
|
134
|
+
message: 'sync.attributes',
|
|
135
|
+
attributes: {
|
|
136
|
+
id: 'abc',
|
|
137
|
+
nested: '{"ok":true}',
|
|
138
|
+
values: '[1,2,3]',
|
|
139
|
+
enabled: true,
|
|
140
|
+
count: 3,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
});
|