@goliapkg/sentori-react-native 0.5.0 → 0.5.2
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/lib/handlers/network.d.ts.map +1 -1
- package/lib/handlers/network.js +88 -2
- package/lib/handlers/network.js.map +1 -1
- package/lib/transport.d.ts +1 -0
- package/lib/transport.d.ts.map +1 -1
- package/lib/transport.js +50 -0
- package/lib/transport.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/tracing.test.ts +85 -1
- package/src/__tests__/transport.test.ts +77 -0
- package/src/handlers/network.ts +112 -1
- package/src/transport.ts +52 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/handlers/network.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/handlers/network.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,qBAAqB,QAAO,IAKxC,CAAC"}
|
package/lib/handlers/network.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import { startSpan } from '@goliapkg/sentori-core';
|
|
2
2
|
import { addBreadcrumb } from '../breadcrumbs';
|
|
3
|
+
import { getConfig } from '../config';
|
|
3
4
|
let _installed = false;
|
|
4
5
|
const AUTH_PARAMS = ['token', 'key', 'password', 'secret', 'access_token'];
|
|
6
|
+
// Requests to our own ingest endpoint shouldn't be traced — otherwise
|
|
7
|
+
// every span upload spawns another http.client span, and so on.
|
|
8
|
+
const isIngestUrl = (url) => {
|
|
9
|
+
const base = getConfig()?.ingestUrl;
|
|
10
|
+
return !!base && url.startsWith(base);
|
|
11
|
+
};
|
|
5
12
|
export const installNetworkHandler = () => {
|
|
6
13
|
if (_installed)
|
|
7
14
|
return;
|
|
15
|
+
_installed = true;
|
|
16
|
+
patchFetch();
|
|
17
|
+
patchXhr();
|
|
18
|
+
};
|
|
19
|
+
// ── fetch ──────────────────────────────────────────────────────────
|
|
20
|
+
function patchFetch() {
|
|
8
21
|
if (typeof globalThis.fetch !== 'function')
|
|
9
22
|
return;
|
|
10
|
-
_installed = true;
|
|
11
23
|
const original = globalThis.fetch;
|
|
12
24
|
globalThis.fetch = (async (input, init) => {
|
|
13
25
|
const start = Date.now();
|
|
14
26
|
const url = extractUrl(input);
|
|
27
|
+
if (isIngestUrl(url))
|
|
28
|
+
return original(input, init);
|
|
15
29
|
const scrubbed = scrubUrl(url);
|
|
16
30
|
const method = (init?.method ??
|
|
17
31
|
(typeof input !== 'string' && 'method' in input
|
|
@@ -63,7 +77,79 @@ export const installNetworkHandler = () => {
|
|
|
63
77
|
throw e;
|
|
64
78
|
}
|
|
65
79
|
});
|
|
66
|
-
}
|
|
80
|
+
}
|
|
81
|
+
function patchXhr() {
|
|
82
|
+
const XHR = globalThis.XMLHttpRequest;
|
|
83
|
+
if (typeof XHR !== 'function')
|
|
84
|
+
return;
|
|
85
|
+
const proto = XHR.prototype;
|
|
86
|
+
if (proto.__sentoriPatched)
|
|
87
|
+
return;
|
|
88
|
+
proto.__sentoriPatched = true;
|
|
89
|
+
const originalOpen = proto.open;
|
|
90
|
+
const originalSend = proto.send;
|
|
91
|
+
const originalSetHeader = proto.setRequestHeader;
|
|
92
|
+
proto.open = function (method, url, ...rest) {
|
|
93
|
+
this.__sentoriMethod = String(method).toUpperCase();
|
|
94
|
+
this.__sentoriUrl = typeof url === 'string' ? url : String(url);
|
|
95
|
+
// @ts-expect-error variadic forwarding to the native signature
|
|
96
|
+
return originalOpen.call(this, method, url, ...rest);
|
|
97
|
+
};
|
|
98
|
+
proto.send = function (body) {
|
|
99
|
+
if (isIngestUrl(this.__sentoriUrl ?? ''))
|
|
100
|
+
return originalSend.call(this, body);
|
|
101
|
+
const method = this.__sentoriMethod ?? 'GET';
|
|
102
|
+
const url = scrubUrl(this.__sentoriUrl ?? '');
|
|
103
|
+
const span = startSpan('http.client', {
|
|
104
|
+
name: `${method} ${url}`,
|
|
105
|
+
tags: { 'http.method': method, 'http.url': url },
|
|
106
|
+
});
|
|
107
|
+
this.__sentoriSpan = span;
|
|
108
|
+
this.__sentoriStart = Date.now();
|
|
109
|
+
// setRequestHeader must be called between open() and send(); we're
|
|
110
|
+
// inside send() before the underlying call, so this is legal.
|
|
111
|
+
try {
|
|
112
|
+
originalSetHeader.call(this, 'traceparent', toTraceparent(span.traceId, span.spanId));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Some XHR polyfills are strict about header timing; if it
|
|
116
|
+
// rejects, drop the header rather than fail the request.
|
|
117
|
+
}
|
|
118
|
+
const finish = () => {
|
|
119
|
+
const s = this.__sentoriSpan;
|
|
120
|
+
if (!s)
|
|
121
|
+
return;
|
|
122
|
+
this.__sentoriSpan = undefined;
|
|
123
|
+
const status = this.status;
|
|
124
|
+
s.setTag('http.status', String(status));
|
|
125
|
+
// status 0 means network error / aborted / CORS block — treat
|
|
126
|
+
// as error. The `abort` event handler below downgrades aborts.
|
|
127
|
+
s.finish({ status: status === 0 || status >= 400 ? 'error' : 'ok' });
|
|
128
|
+
addBreadcrumb({
|
|
129
|
+
type: 'net',
|
|
130
|
+
data: {
|
|
131
|
+
method,
|
|
132
|
+
url,
|
|
133
|
+
status,
|
|
134
|
+
durationMs: Date.now() - (this.__sentoriStart ?? Date.now()),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
this.addEventListener('loadend', finish);
|
|
139
|
+
this.addEventListener('abort', () => {
|
|
140
|
+
const s = this.__sentoriSpan;
|
|
141
|
+
if (!s)
|
|
142
|
+
return;
|
|
143
|
+
this.__sentoriSpan = undefined;
|
|
144
|
+
s.finish({ status: 'cancelled' });
|
|
145
|
+
addBreadcrumb({
|
|
146
|
+
type: 'net',
|
|
147
|
+
data: { method, url, status: 0, durationMs: Date.now() - (this.__sentoriStart ?? Date.now()), error: 'aborted' },
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
return originalSend.call(this, body);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
67
153
|
function mergeHeaders(input, init) {
|
|
68
154
|
const out = new Headers();
|
|
69
155
|
if (typeof input !== 'string' && !(input instanceof URL)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/handlers/network.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/handlers/network.ts"],"names":[],"mappings":"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"}
|
package/lib/transport.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Event } from './types';
|
|
2
2
|
export declare const enqueue: (event: Event) => void;
|
|
3
3
|
export declare const startTransport: () => void;
|
|
4
|
+
export declare const flushSpans: () => Promise<void>;
|
|
4
5
|
export declare const flush: () => Promise<void>;
|
|
5
6
|
export declare const drainOfflineQueue: () => Promise<void>;
|
|
6
7
|
export declare const __resetForTests: () => void;
|
package/lib/transport.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAqBrC,eAAO,MAAM,OAAO,GAAI,OAAO,KAAK,KAAG,IAUtC,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,IAOjC,CAAC;AAEF,eAAO,MAAM,UAAU,QAAa,OAAO,CAAC,IAAI,CAe/C,CAAC;AAqBF,eAAO,MAAM,KAAK,QAAa,OAAO,CAAC,IAAI,CAkB1C,CAAC;AA4FF,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,IAAI,CAatD,CAAC;AAEF,eAAO,MAAM,eAAe,QAAO,IAOlC,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,SAAS,KAAK,EAAY,CAAC;AAE1D;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,WAAW,MAAM,EACjB,OAAO,MAAM,EACb,MAAM,OAAO,KACZ,OAAO,CAAC,IAAI,CAcd,CAAC"}
|
package/lib/transport.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import { drainSpans } from '@goliapkg/sentori-core';
|
|
1
2
|
import { getConfig } from './config';
|
|
2
3
|
const FLUSH_INTERVAL_MS = 5_000;
|
|
3
4
|
const BATCH_SIZE = 10;
|
|
4
5
|
const MAX_RETRY = 3;
|
|
5
6
|
const STORAGE_KEY = '@sentori/pending';
|
|
6
7
|
const MAX_PERSISTED = 1000;
|
|
8
|
+
// Spans are higher-volume and lower-value than error events: we batch
|
|
9
|
+
// them on their own timer, cap each request at the server's per-batch
|
|
10
|
+
// limit, and drop on failure rather than persisting offline.
|
|
11
|
+
const SPAN_FLUSH_INTERVAL_MS = 10_000;
|
|
12
|
+
const SPAN_BATCH_MAX = 200;
|
|
7
13
|
let _queue = [];
|
|
8
14
|
let _flushTimer = null;
|
|
15
|
+
let _spanTimer = null;
|
|
9
16
|
let _started = false;
|
|
10
17
|
const SDK_VERSION = '0.0.0';
|
|
11
18
|
export const enqueue = (event) => {
|
|
@@ -22,6 +29,46 @@ export const enqueue = (event) => {
|
|
|
22
29
|
};
|
|
23
30
|
export const startTransport = () => {
|
|
24
31
|
_started = true;
|
|
32
|
+
if (!_spanTimer) {
|
|
33
|
+
_spanTimer = setInterval(() => {
|
|
34
|
+
void flushSpans();
|
|
35
|
+
}, SPAN_FLUSH_INTERVAL_MS);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
export const flushSpans = async () => {
|
|
39
|
+
if (!_started)
|
|
40
|
+
return;
|
|
41
|
+
const spans = drainSpans();
|
|
42
|
+
if (spans.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
const config = getConfig();
|
|
45
|
+
if (!config)
|
|
46
|
+
return;
|
|
47
|
+
for (let i = 0; i < spans.length; i += SPAN_BATCH_MAX) {
|
|
48
|
+
const chunk = spans.slice(i, i + SPAN_BATCH_MAX);
|
|
49
|
+
try {
|
|
50
|
+
await sendSpansOnce(chunk, config.ingestUrl, config.token);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// drop the remaining chunks — span uploads aren't worth retrying
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const sendSpansOnce = async (spans, ingestUrl, token) => {
|
|
59
|
+
const resp = await fetch(`${ingestUrl}/v1/spans:batch`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
Authorization: `Bearer ${token}`,
|
|
64
|
+
'Sentori-Sdk': `react-native/${SDK_VERSION}`,
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify({ spans }),
|
|
67
|
+
});
|
|
68
|
+
// 5xx: server overloaded. 4xx (bad token / quota / oversized): also
|
|
69
|
+
// pointless to keep sending. Either way, stop the rest of the batch.
|
|
70
|
+
if (resp.status >= 400)
|
|
71
|
+
throw new Error(`spans-${resp.status}`);
|
|
25
72
|
};
|
|
26
73
|
export const flush = async () => {
|
|
27
74
|
if (!_started)
|
|
@@ -137,6 +184,9 @@ export const __resetForTests = () => {
|
|
|
137
184
|
if (_flushTimer)
|
|
138
185
|
clearTimeout(_flushTimer);
|
|
139
186
|
_flushTimer = null;
|
|
187
|
+
if (_spanTimer)
|
|
188
|
+
clearInterval(_spanTimer);
|
|
189
|
+
_spanTimer = null;
|
|
140
190
|
_started = false;
|
|
141
191
|
};
|
|
142
192
|
export const __peekQueue = () => _queue;
|
package/lib/transport.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGrC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAChC,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,SAAS,GAAG,CAAC,CAAC;AACpB,MAAM,WAAW,GAAG,kBAAkB,CAAC;AACvC,MAAM,aAAa,GAAG,IAAI,CAAC;AAE3B,IAAI,MAAM,GAAY,EAAE,CAAC;AACzB,IAAI,WAAW,GAAyC,IAAI,CAAC;AAC7D,IAAI,QAAQ,GAAG,KAAK,CAAC;AAErB,MAAM,WAAW,GAAG,OAAO,CAAC;AAE5B,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,KAAY,EAAQ,EAAE;IAC5C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnB,IAAI,MAAM,CAAC,MAAM,IAAI,UAAU,EAAE,CAAC;QAChC,KAAK,KAAK,EAAE,CAAC;IACf,CAAC;SAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACxB,WAAW,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,WAAW,GAAG,IAAI,CAAC;YACnB,KAAK,KAAK,EAAE,CAAC;QACf,CAAC,EAAE,iBAAiB,CAAC,CAAC;IACxB,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,GAAS,EAAE;IACvC,QAAQ,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/sentori-react-native",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Sentori SDK for React Native
|
|
3
|
+
"version": "0.5.2",
|
|
4
|
+
"description": "Sentori SDK for React Native \u2014 JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://sentori.golia.jp",
|
|
7
7
|
"repository": {
|
|
@@ -33,9 +33,38 @@ const recorder = (async (input: Request | string | URL, init?: RequestInit) => {
|
|
|
33
33
|
return new Response('', { status: r.status });
|
|
34
34
|
}) as unknown as typeof fetch;
|
|
35
35
|
|
|
36
|
+
// Fake XMLHttpRequest so installNetworkHandler()'s patchXhr() has a
|
|
37
|
+
// prototype to patch. RN's native XHR isn't present in bun:test.
|
|
38
|
+
type FakeListener = () => void;
|
|
39
|
+
class FakeXHR {
|
|
40
|
+
status = 0;
|
|
41
|
+
private listeners: Record<string, FakeListener[]> = {};
|
|
42
|
+
private requestHeaders: Record<string, string> = {};
|
|
43
|
+
private opened = false;
|
|
44
|
+
|
|
45
|
+
open(_method: string, _url: string | URL): void {
|
|
46
|
+
this.opened = true;
|
|
47
|
+
}
|
|
48
|
+
setRequestHeader(name: string, value: string): void {
|
|
49
|
+
if (!this.opened) throw new Error('setRequestHeader before open');
|
|
50
|
+
this.requestHeaders[name.toLowerCase()] = value;
|
|
51
|
+
}
|
|
52
|
+
send(_body?: unknown): void {}
|
|
53
|
+
addEventListener(event: string, fn: FakeListener): void {
|
|
54
|
+
(this.listeners[event] ??= []).push(fn);
|
|
55
|
+
}
|
|
56
|
+
getHeader(name: string): string | undefined {
|
|
57
|
+
return this.requestHeaders[name.toLowerCase()];
|
|
58
|
+
}
|
|
59
|
+
fire(event: string): void {
|
|
60
|
+
for (const fn of this.listeners[event] ?? []) fn();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
beforeAll(() => {
|
|
37
65
|
(globalThis as { fetch: typeof fetch }).fetch = recorder;
|
|
38
|
-
|
|
66
|
+
(globalThis as { XMLHttpRequest: unknown }).XMLHttpRequest = FakeXHR as unknown;
|
|
67
|
+
installNetworkHandler(); // patches globalThis.fetch + XMLHttpRequest.prototype
|
|
39
68
|
});
|
|
40
69
|
|
|
41
70
|
beforeEach(() => {
|
|
@@ -106,3 +135,58 @@ describe('RN network handler tracing', () => {
|
|
|
106
135
|
expect(h.get('traceparent')).toBeTruthy();
|
|
107
136
|
});
|
|
108
137
|
});
|
|
138
|
+
|
|
139
|
+
describe('RN XHR tracing (axios goes through XHR on RN)', () => {
|
|
140
|
+
test('patched XHR emits an http.client span on loadend', () => {
|
|
141
|
+
const x = new FakeXHR();
|
|
142
|
+
x.open('POST', 'https://api.example.com/v1/orders');
|
|
143
|
+
x.send('{}');
|
|
144
|
+
x.status = 201;
|
|
145
|
+
x.fire('loadend');
|
|
146
|
+
|
|
147
|
+
const sp = drainSpans()[0]!;
|
|
148
|
+
expect(sp.op).toBe('http.client');
|
|
149
|
+
expect(sp.name).toBe('POST https://api.example.com/v1/orders');
|
|
150
|
+
expect(sp.tags).toMatchObject({
|
|
151
|
+
'http.method': 'POST',
|
|
152
|
+
'http.status': '201',
|
|
153
|
+
'http.url': 'https://api.example.com/v1/orders',
|
|
154
|
+
});
|
|
155
|
+
expect(sp.status).toBe('ok');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('injects W3C traceparent request header', () => {
|
|
159
|
+
const x = new FakeXHR();
|
|
160
|
+
x.open('GET', 'https://api.example.com/x');
|
|
161
|
+
x.send();
|
|
162
|
+
expect(x.getHeader('traceparent')).toMatch(/^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/);
|
|
163
|
+
x.status = 200;
|
|
164
|
+
x.fire('loadend');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('5xx → span.status = "error"', () => {
|
|
168
|
+
const x = new FakeXHR();
|
|
169
|
+
x.open('GET', 'https://api.example.com/x');
|
|
170
|
+
x.send();
|
|
171
|
+
x.status = 502;
|
|
172
|
+
x.fire('loadend');
|
|
173
|
+
expect(drainSpans()[0]?.status).toBe('error');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('status 0 → span.status = "error"', () => {
|
|
177
|
+
const x = new FakeXHR();
|
|
178
|
+
x.open('GET', 'https://api.example.com/x');
|
|
179
|
+
x.send();
|
|
180
|
+
x.status = 0;
|
|
181
|
+
x.fire('loadend');
|
|
182
|
+
expect(drainSpans()[0]?.status).toBe('error');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('abort → span.status = "cancelled"', () => {
|
|
186
|
+
const x = new FakeXHR();
|
|
187
|
+
x.open('GET', 'https://api.example.com/x');
|
|
188
|
+
x.send();
|
|
189
|
+
x.fire('abort');
|
|
190
|
+
expect(drainSpans()[0]?.status).toBe('cancelled');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -7,10 +7,13 @@ import {
|
|
|
7
7
|
mock,
|
|
8
8
|
} from 'bun:test';
|
|
9
9
|
|
|
10
|
+
import { clearSpans, startSpan } from '@goliapkg/sentori-core';
|
|
11
|
+
|
|
10
12
|
import { setConfig, __resetForTests as resetConfig } from '../config';
|
|
11
13
|
import {
|
|
12
14
|
enqueue,
|
|
13
15
|
flush,
|
|
16
|
+
flushSpans,
|
|
14
17
|
startTransport,
|
|
15
18
|
__resetForTests as resetTransport,
|
|
16
19
|
__peekQueue,
|
|
@@ -168,3 +171,77 @@ describe('transport', () => {
|
|
|
168
171
|
expect(__peekQueue()).toHaveLength(0);
|
|
169
172
|
});
|
|
170
173
|
});
|
|
174
|
+
|
|
175
|
+
describe('span flush', () => {
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
resetConfig();
|
|
178
|
+
resetTransport();
|
|
179
|
+
clearSpans();
|
|
180
|
+
setConfig({
|
|
181
|
+
token: 'st_pk_test',
|
|
182
|
+
release: 'app@1.0.0+1',
|
|
183
|
+
environment: 'test',
|
|
184
|
+
ingestUrl: 'http://localhost:8080',
|
|
185
|
+
enabled: true,
|
|
186
|
+
});
|
|
187
|
+
startTransport();
|
|
188
|
+
});
|
|
189
|
+
afterEach(() => {
|
|
190
|
+
globalThis.fetch = originalFetch;
|
|
191
|
+
clearSpans();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('POSTs buffered spans to /v1/spans:batch', async () => {
|
|
195
|
+
const calls: { url: string; init?: RequestInit }[] = [];
|
|
196
|
+
globalThis.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => {
|
|
197
|
+
calls.push({ url: String(url), init });
|
|
198
|
+
return new Response(null, { status: 202 });
|
|
199
|
+
}) as typeof fetch;
|
|
200
|
+
|
|
201
|
+
startSpan('http.client', { name: 'GET /a' }).finish({ status: 'ok' });
|
|
202
|
+
startSpan('http.client', { name: 'GET /b' }).finish({ status: 'error' });
|
|
203
|
+
await flushSpans();
|
|
204
|
+
|
|
205
|
+
expect(calls).toHaveLength(1);
|
|
206
|
+
expect(calls[0]?.url).toBe('http://localhost:8080/v1/spans:batch');
|
|
207
|
+
const body = JSON.parse((calls[0]?.init?.body as string) ?? '{}');
|
|
208
|
+
expect(body.spans).toHaveLength(2);
|
|
209
|
+
expect(body.spans[0]).toMatchObject({ op: 'http.client', name: 'GET /a', status: 'ok' });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('no-op when the span buffer is empty', async () => {
|
|
213
|
+
let attempts = 0;
|
|
214
|
+
globalThis.fetch = mock(async () => {
|
|
215
|
+
attempts++;
|
|
216
|
+
return new Response(null, { status: 202 });
|
|
217
|
+
}) as typeof fetch;
|
|
218
|
+
await flushSpans();
|
|
219
|
+
expect(attempts).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('splits >200 spans into multiple batches', async () => {
|
|
223
|
+
const sizes: number[] = [];
|
|
224
|
+
globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
|
|
225
|
+
const body = JSON.parse((init?.body as string) ?? '{}');
|
|
226
|
+
sizes.push(body.spans.length);
|
|
227
|
+
return new Response(null, { status: 202 });
|
|
228
|
+
}) as typeof fetch;
|
|
229
|
+
|
|
230
|
+
for (let i = 0; i < 450; i++) startSpan('http.client', { name: `GET /${i}` }).finish();
|
|
231
|
+
await flushSpans();
|
|
232
|
+
|
|
233
|
+
expect(sizes).toEqual([200, 200, 50]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('drops on 5xx without retrying', async () => {
|
|
237
|
+
let attempts = 0;
|
|
238
|
+
globalThis.fetch = mock(async () => {
|
|
239
|
+
attempts++;
|
|
240
|
+
return new Response('boom', { status: 503 });
|
|
241
|
+
}) as typeof fetch;
|
|
242
|
+
|
|
243
|
+
startSpan('http.client', { name: 'GET /a' }).finish();
|
|
244
|
+
await flushSpans();
|
|
245
|
+
expect(attempts).toBe(1);
|
|
246
|
+
});
|
|
247
|
+
});
|
package/src/handlers/network.ts
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
import { startSpan } from '@goliapkg/sentori-core';
|
|
2
2
|
|
|
3
3
|
import { addBreadcrumb } from '../breadcrumbs';
|
|
4
|
+
import { getConfig } from '../config';
|
|
4
5
|
|
|
5
6
|
let _installed = false;
|
|
6
7
|
|
|
7
8
|
const AUTH_PARAMS = ['token', 'key', 'password', 'secret', 'access_token'];
|
|
8
9
|
|
|
10
|
+
// Requests to our own ingest endpoint shouldn't be traced — otherwise
|
|
11
|
+
// every span upload spawns another http.client span, and so on.
|
|
12
|
+
const isIngestUrl = (url: string): boolean => {
|
|
13
|
+
const base = getConfig()?.ingestUrl;
|
|
14
|
+
return !!base && url.startsWith(base);
|
|
15
|
+
};
|
|
16
|
+
|
|
9
17
|
export const installNetworkHandler = (): void => {
|
|
10
18
|
if (_installed) return;
|
|
11
|
-
if (typeof globalThis.fetch !== 'function') return;
|
|
12
19
|
_installed = true;
|
|
20
|
+
patchFetch();
|
|
21
|
+
patchXhr();
|
|
22
|
+
};
|
|
13
23
|
|
|
24
|
+
// ── fetch ──────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function patchFetch(): void {
|
|
27
|
+
if (typeof globalThis.fetch !== 'function') return;
|
|
14
28
|
const original = globalThis.fetch;
|
|
15
29
|
|
|
16
30
|
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
17
31
|
const start = Date.now();
|
|
18
32
|
const url = extractUrl(input);
|
|
33
|
+
if (isIngestUrl(url)) return original(input, init);
|
|
19
34
|
const scrubbed = scrubUrl(url);
|
|
20
35
|
const method = (init?.method ??
|
|
21
36
|
(typeof input !== 'string' && 'method' in (input as Request)
|
|
@@ -68,8 +83,104 @@ export const installNetworkHandler = (): void => {
|
|
|
68
83
|
throw e;
|
|
69
84
|
}
|
|
70
85
|
}) as typeof fetch;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── XMLHttpRequest ─────────────────────────────────────────────────
|
|
89
|
+
//
|
|
90
|
+
// React Native's XHR is a native polyfill, not built on fetch — so
|
|
91
|
+
// patching `globalThis.fetch` alone misses every axios / older-style
|
|
92
|
+
// request. axios on RN uses its `xhr` adapter by default. We patch
|
|
93
|
+
// the prototype's `open` + `send` so the instance carries the span
|
|
94
|
+
// from `send()` to `loadend`.
|
|
95
|
+
|
|
96
|
+
type TracedXhr = XMLHttpRequest & {
|
|
97
|
+
__sentoriMethod?: string;
|
|
98
|
+
__sentoriUrl?: string;
|
|
99
|
+
__sentoriSpan?: ReturnType<typeof startSpan>;
|
|
100
|
+
__sentoriStart?: number;
|
|
71
101
|
};
|
|
72
102
|
|
|
103
|
+
function patchXhr(): void {
|
|
104
|
+
const XHR = (globalThis as { XMLHttpRequest?: typeof XMLHttpRequest }).XMLHttpRequest;
|
|
105
|
+
if (typeof XHR !== 'function') return;
|
|
106
|
+
const proto = XHR.prototype as XMLHttpRequest & {
|
|
107
|
+
__sentoriPatched?: boolean;
|
|
108
|
+
};
|
|
109
|
+
if (proto.__sentoriPatched) return;
|
|
110
|
+
proto.__sentoriPatched = true;
|
|
111
|
+
|
|
112
|
+
const originalOpen = proto.open;
|
|
113
|
+
const originalSend = proto.send;
|
|
114
|
+
const originalSetHeader = proto.setRequestHeader;
|
|
115
|
+
|
|
116
|
+
proto.open = function (
|
|
117
|
+
this: TracedXhr,
|
|
118
|
+
method: string,
|
|
119
|
+
url: string | URL,
|
|
120
|
+
...rest: unknown[]
|
|
121
|
+
): void {
|
|
122
|
+
this.__sentoriMethod = String(method).toUpperCase();
|
|
123
|
+
this.__sentoriUrl = typeof url === 'string' ? url : String(url);
|
|
124
|
+
// @ts-expect-error variadic forwarding to the native signature
|
|
125
|
+
return originalOpen.call(this, method, url, ...rest);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
proto.send = function (this: TracedXhr, body?: Document | XMLHttpRequestBodyInit | null): void {
|
|
129
|
+
if (isIngestUrl(this.__sentoriUrl ?? '')) return originalSend.call(this, body);
|
|
130
|
+
const method = this.__sentoriMethod ?? 'GET';
|
|
131
|
+
const url = scrubUrl(this.__sentoriUrl ?? '');
|
|
132
|
+
const span = startSpan('http.client', {
|
|
133
|
+
name: `${method} ${url}`,
|
|
134
|
+
tags: { 'http.method': method, 'http.url': url },
|
|
135
|
+
});
|
|
136
|
+
this.__sentoriSpan = span;
|
|
137
|
+
this.__sentoriStart = Date.now();
|
|
138
|
+
|
|
139
|
+
// setRequestHeader must be called between open() and send(); we're
|
|
140
|
+
// inside send() before the underlying call, so this is legal.
|
|
141
|
+
try {
|
|
142
|
+
originalSetHeader.call(this, 'traceparent', toTraceparent(span.traceId, span.spanId));
|
|
143
|
+
} catch {
|
|
144
|
+
// Some XHR polyfills are strict about header timing; if it
|
|
145
|
+
// rejects, drop the header rather than fail the request.
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const finish = () => {
|
|
149
|
+
const s = this.__sentoriSpan;
|
|
150
|
+
if (!s) return;
|
|
151
|
+
this.__sentoriSpan = undefined;
|
|
152
|
+
const status = this.status;
|
|
153
|
+
s.setTag('http.status', String(status));
|
|
154
|
+
// status 0 means network error / aborted / CORS block — treat
|
|
155
|
+
// as error. The `abort` event handler below downgrades aborts.
|
|
156
|
+
s.finish({ status: status === 0 || status >= 400 ? 'error' : 'ok' });
|
|
157
|
+
addBreadcrumb({
|
|
158
|
+
type: 'net',
|
|
159
|
+
data: {
|
|
160
|
+
method,
|
|
161
|
+
url,
|
|
162
|
+
status,
|
|
163
|
+
durationMs: Date.now() - (this.__sentoriStart ?? Date.now()),
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
this.addEventListener('loadend', finish);
|
|
169
|
+
this.addEventListener('abort', () => {
|
|
170
|
+
const s = this.__sentoriSpan;
|
|
171
|
+
if (!s) return;
|
|
172
|
+
this.__sentoriSpan = undefined;
|
|
173
|
+
s.finish({ status: 'cancelled' });
|
|
174
|
+
addBreadcrumb({
|
|
175
|
+
type: 'net',
|
|
176
|
+
data: { method, url, status: 0, durationMs: Date.now() - (this.__sentoriStart ?? Date.now()), error: 'aborted' },
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return originalSend.call(this, body);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
73
184
|
function mergeHeaders(input: RequestInfo | URL, init?: RequestInit): Headers {
|
|
74
185
|
const out = new Headers();
|
|
75
186
|
if (typeof input !== 'string' && !(input instanceof URL)) {
|
package/src/transport.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { drainSpans } from '@goliapkg/sentori-core';
|
|
2
|
+
|
|
1
3
|
import { getConfig } from './config';
|
|
2
4
|
import type { Event } from './types';
|
|
3
5
|
|
|
@@ -7,8 +9,15 @@ const MAX_RETRY = 3;
|
|
|
7
9
|
const STORAGE_KEY = '@sentori/pending';
|
|
8
10
|
const MAX_PERSISTED = 1000;
|
|
9
11
|
|
|
12
|
+
// Spans are higher-volume and lower-value than error events: we batch
|
|
13
|
+
// them on their own timer, cap each request at the server's per-batch
|
|
14
|
+
// limit, and drop on failure rather than persisting offline.
|
|
15
|
+
const SPAN_FLUSH_INTERVAL_MS = 10_000;
|
|
16
|
+
const SPAN_BATCH_MAX = 200;
|
|
17
|
+
|
|
10
18
|
let _queue: Event[] = [];
|
|
11
19
|
let _flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
20
|
+
let _spanTimer: ReturnType<typeof setInterval> | null = null;
|
|
12
21
|
let _started = false;
|
|
13
22
|
|
|
14
23
|
const SDK_VERSION = '0.0.0';
|
|
@@ -27,6 +36,47 @@ export const enqueue = (event: Event): void => {
|
|
|
27
36
|
|
|
28
37
|
export const startTransport = (): void => {
|
|
29
38
|
_started = true;
|
|
39
|
+
if (!_spanTimer) {
|
|
40
|
+
_spanTimer = setInterval(() => {
|
|
41
|
+
void flushSpans();
|
|
42
|
+
}, SPAN_FLUSH_INTERVAL_MS);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const flushSpans = async (): Promise<void> => {
|
|
47
|
+
if (!_started) return;
|
|
48
|
+
const spans = drainSpans();
|
|
49
|
+
if (spans.length === 0) return;
|
|
50
|
+
const config = getConfig();
|
|
51
|
+
if (!config) return;
|
|
52
|
+
for (let i = 0; i < spans.length; i += SPAN_BATCH_MAX) {
|
|
53
|
+
const chunk = spans.slice(i, i + SPAN_BATCH_MAX);
|
|
54
|
+
try {
|
|
55
|
+
await sendSpansOnce(chunk, config.ingestUrl, config.token);
|
|
56
|
+
} catch {
|
|
57
|
+
// drop the remaining chunks — span uploads aren't worth retrying
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const sendSpansOnce = async (
|
|
64
|
+
spans: unknown[],
|
|
65
|
+
ingestUrl: string,
|
|
66
|
+
token: string,
|
|
67
|
+
): Promise<void> => {
|
|
68
|
+
const resp = await fetch(`${ingestUrl}/v1/spans:batch`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
Authorization: `Bearer ${token}`,
|
|
73
|
+
'Sentori-Sdk': `react-native/${SDK_VERSION}`,
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify({ spans }),
|
|
76
|
+
});
|
|
77
|
+
// 5xx: server overloaded. 4xx (bad token / quota / oversized): also
|
|
78
|
+
// pointless to keep sending. Either way, stop the rest of the batch.
|
|
79
|
+
if (resp.status >= 400) throw new Error(`spans-${resp.status}`);
|
|
30
80
|
};
|
|
31
81
|
|
|
32
82
|
export const flush = async (): Promise<void> => {
|
|
@@ -158,6 +208,8 @@ export const __resetForTests = (): void => {
|
|
|
158
208
|
_queue = [];
|
|
159
209
|
if (_flushTimer) clearTimeout(_flushTimer);
|
|
160
210
|
_flushTimer = null;
|
|
211
|
+
if (_spanTimer) clearInterval(_spanTimer);
|
|
212
|
+
_spanTimer = null;
|
|
161
213
|
_started = false;
|
|
162
214
|
};
|
|
163
215
|
|