@goliapkg/sentori-react-native 0.5.0 → 0.5.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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/handlers/network.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,qBAAqB,QAAO,
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../src/handlers/network.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,qBAAqB,QAAO,IAKxC,CAAC"}
|
package/lib/handlers/network.js
CHANGED
|
@@ -5,9 +5,14 @@ const AUTH_PARAMS = ['token', 'key', 'password', 'secret', 'access_token'];
|
|
|
5
5
|
export const installNetworkHandler = () => {
|
|
6
6
|
if (_installed)
|
|
7
7
|
return;
|
|
8
|
+
_installed = true;
|
|
9
|
+
patchFetch();
|
|
10
|
+
patchXhr();
|
|
11
|
+
};
|
|
12
|
+
// ── fetch ──────────────────────────────────────────────────────────
|
|
13
|
+
function patchFetch() {
|
|
8
14
|
if (typeof globalThis.fetch !== 'function')
|
|
9
15
|
return;
|
|
10
|
-
_installed = true;
|
|
11
16
|
const original = globalThis.fetch;
|
|
12
17
|
globalThis.fetch = (async (input, init) => {
|
|
13
18
|
const start = Date.now();
|
|
@@ -63,7 +68,77 @@ export const installNetworkHandler = () => {
|
|
|
63
68
|
throw e;
|
|
64
69
|
}
|
|
65
70
|
});
|
|
66
|
-
}
|
|
71
|
+
}
|
|
72
|
+
function patchXhr() {
|
|
73
|
+
const XHR = globalThis.XMLHttpRequest;
|
|
74
|
+
if (typeof XHR !== 'function')
|
|
75
|
+
return;
|
|
76
|
+
const proto = XHR.prototype;
|
|
77
|
+
if (proto.__sentoriPatched)
|
|
78
|
+
return;
|
|
79
|
+
proto.__sentoriPatched = true;
|
|
80
|
+
const originalOpen = proto.open;
|
|
81
|
+
const originalSend = proto.send;
|
|
82
|
+
const originalSetHeader = proto.setRequestHeader;
|
|
83
|
+
proto.open = function (method, url, ...rest) {
|
|
84
|
+
this.__sentoriMethod = String(method).toUpperCase();
|
|
85
|
+
this.__sentoriUrl = typeof url === 'string' ? url : String(url);
|
|
86
|
+
// @ts-expect-error variadic forwarding to the native signature
|
|
87
|
+
return originalOpen.call(this, method, url, ...rest);
|
|
88
|
+
};
|
|
89
|
+
proto.send = function (body) {
|
|
90
|
+
const method = this.__sentoriMethod ?? 'GET';
|
|
91
|
+
const url = scrubUrl(this.__sentoriUrl ?? '');
|
|
92
|
+
const span = startSpan('http.client', {
|
|
93
|
+
name: `${method} ${url}`,
|
|
94
|
+
tags: { 'http.method': method, 'http.url': url },
|
|
95
|
+
});
|
|
96
|
+
this.__sentoriSpan = span;
|
|
97
|
+
this.__sentoriStart = Date.now();
|
|
98
|
+
// setRequestHeader must be called between open() and send(); we're
|
|
99
|
+
// inside send() before the underlying call, so this is legal.
|
|
100
|
+
try {
|
|
101
|
+
originalSetHeader.call(this, 'traceparent', toTraceparent(span.traceId, span.spanId));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Some XHR polyfills are strict about header timing; if it
|
|
105
|
+
// rejects, drop the header rather than fail the request.
|
|
106
|
+
}
|
|
107
|
+
const finish = () => {
|
|
108
|
+
const s = this.__sentoriSpan;
|
|
109
|
+
if (!s)
|
|
110
|
+
return;
|
|
111
|
+
this.__sentoriSpan = undefined;
|
|
112
|
+
const status = this.status;
|
|
113
|
+
s.setTag('http.status', String(status));
|
|
114
|
+
// status 0 means network error / aborted / CORS block — treat
|
|
115
|
+
// as error. The `abort` event handler below downgrades aborts.
|
|
116
|
+
s.finish({ status: status === 0 || status >= 400 ? 'error' : 'ok' });
|
|
117
|
+
addBreadcrumb({
|
|
118
|
+
type: 'net',
|
|
119
|
+
data: {
|
|
120
|
+
method,
|
|
121
|
+
url,
|
|
122
|
+
status,
|
|
123
|
+
durationMs: Date.now() - (this.__sentoriStart ?? Date.now()),
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
this.addEventListener('loadend', finish);
|
|
128
|
+
this.addEventListener('abort', () => {
|
|
129
|
+
const s = this.__sentoriSpan;
|
|
130
|
+
if (!s)
|
|
131
|
+
return;
|
|
132
|
+
this.__sentoriSpan = undefined;
|
|
133
|
+
s.finish({ status: 'cancelled' });
|
|
134
|
+
addBreadcrumb({
|
|
135
|
+
type: 'net',
|
|
136
|
+
data: { method, url, status: 0, durationMs: Date.now() - (this.__sentoriStart ?? Date.now()), error: 'aborted' },
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
return originalSend.call(this, body);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
67
142
|
function mergeHeaders(input, init) {
|
|
68
143
|
const out = new Headers();
|
|
69
144
|
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;AAE/C,IAAI,UAAU,GAAG,KAAK,CAAC;AAEvB,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;AAE3E,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAS,EAAE;IAC9C,IAAI,UAAU;QAAE,OAAO;IACvB,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,UAAU;QAAE,OAAO;IACnD,
|
|
1
|
+
{"version":3,"file":"network.js","sourceRoot":"","sources":["../../src/handlers/network.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/sentori-react-native",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Sentori SDK for React Native — 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",
|
|
@@ -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
|
+
});
|
package/src/handlers/network.ts
CHANGED
|
@@ -8,9 +8,15 @@ const AUTH_PARAMS = ['token', 'key', 'password', 'secret', 'access_token'];
|
|
|
8
8
|
|
|
9
9
|
export const installNetworkHandler = (): void => {
|
|
10
10
|
if (_installed) return;
|
|
11
|
-
if (typeof globalThis.fetch !== 'function') return;
|
|
12
11
|
_installed = true;
|
|
12
|
+
patchFetch();
|
|
13
|
+
patchXhr();
|
|
14
|
+
};
|
|
13
15
|
|
|
16
|
+
// ── fetch ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function patchFetch(): void {
|
|
19
|
+
if (typeof globalThis.fetch !== 'function') return;
|
|
14
20
|
const original = globalThis.fetch;
|
|
15
21
|
|
|
16
22
|
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
@@ -68,8 +74,103 @@ export const installNetworkHandler = (): void => {
|
|
|
68
74
|
throw e;
|
|
69
75
|
}
|
|
70
76
|
}) as typeof fetch;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── XMLHttpRequest ─────────────────────────────────────────────────
|
|
80
|
+
//
|
|
81
|
+
// React Native's XHR is a native polyfill, not built on fetch — so
|
|
82
|
+
// patching `globalThis.fetch` alone misses every axios / older-style
|
|
83
|
+
// request. axios on RN uses its `xhr` adapter by default. We patch
|
|
84
|
+
// the prototype's `open` + `send` so the instance carries the span
|
|
85
|
+
// from `send()` to `loadend`.
|
|
86
|
+
|
|
87
|
+
type TracedXhr = XMLHttpRequest & {
|
|
88
|
+
__sentoriMethod?: string;
|
|
89
|
+
__sentoriUrl?: string;
|
|
90
|
+
__sentoriSpan?: ReturnType<typeof startSpan>;
|
|
91
|
+
__sentoriStart?: number;
|
|
71
92
|
};
|
|
72
93
|
|
|
94
|
+
function patchXhr(): void {
|
|
95
|
+
const XHR = (globalThis as { XMLHttpRequest?: typeof XMLHttpRequest }).XMLHttpRequest;
|
|
96
|
+
if (typeof XHR !== 'function') return;
|
|
97
|
+
const proto = XHR.prototype as XMLHttpRequest & {
|
|
98
|
+
__sentoriPatched?: boolean;
|
|
99
|
+
};
|
|
100
|
+
if (proto.__sentoriPatched) return;
|
|
101
|
+
proto.__sentoriPatched = true;
|
|
102
|
+
|
|
103
|
+
const originalOpen = proto.open;
|
|
104
|
+
const originalSend = proto.send;
|
|
105
|
+
const originalSetHeader = proto.setRequestHeader;
|
|
106
|
+
|
|
107
|
+
proto.open = function (
|
|
108
|
+
this: TracedXhr,
|
|
109
|
+
method: string,
|
|
110
|
+
url: string | URL,
|
|
111
|
+
...rest: unknown[]
|
|
112
|
+
): void {
|
|
113
|
+
this.__sentoriMethod = String(method).toUpperCase();
|
|
114
|
+
this.__sentoriUrl = typeof url === 'string' ? url : String(url);
|
|
115
|
+
// @ts-expect-error variadic forwarding to the native signature
|
|
116
|
+
return originalOpen.call(this, method, url, ...rest);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
proto.send = function (this: TracedXhr, body?: Document | XMLHttpRequestBodyInit | null): void {
|
|
120
|
+
const method = this.__sentoriMethod ?? 'GET';
|
|
121
|
+
const url = scrubUrl(this.__sentoriUrl ?? '');
|
|
122
|
+
const span = startSpan('http.client', {
|
|
123
|
+
name: `${method} ${url}`,
|
|
124
|
+
tags: { 'http.method': method, 'http.url': url },
|
|
125
|
+
});
|
|
126
|
+
this.__sentoriSpan = span;
|
|
127
|
+
this.__sentoriStart = Date.now();
|
|
128
|
+
|
|
129
|
+
// setRequestHeader must be called between open() and send(); we're
|
|
130
|
+
// inside send() before the underlying call, so this is legal.
|
|
131
|
+
try {
|
|
132
|
+
originalSetHeader.call(this, 'traceparent', toTraceparent(span.traceId, span.spanId));
|
|
133
|
+
} catch {
|
|
134
|
+
// Some XHR polyfills are strict about header timing; if it
|
|
135
|
+
// rejects, drop the header rather than fail the request.
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const finish = () => {
|
|
139
|
+
const s = this.__sentoriSpan;
|
|
140
|
+
if (!s) return;
|
|
141
|
+
this.__sentoriSpan = undefined;
|
|
142
|
+
const status = this.status;
|
|
143
|
+
s.setTag('http.status', String(status));
|
|
144
|
+
// status 0 means network error / aborted / CORS block — treat
|
|
145
|
+
// as error. The `abort` event handler below downgrades aborts.
|
|
146
|
+
s.finish({ status: status === 0 || status >= 400 ? 'error' : 'ok' });
|
|
147
|
+
addBreadcrumb({
|
|
148
|
+
type: 'net',
|
|
149
|
+
data: {
|
|
150
|
+
method,
|
|
151
|
+
url,
|
|
152
|
+
status,
|
|
153
|
+
durationMs: Date.now() - (this.__sentoriStart ?? Date.now()),
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
this.addEventListener('loadend', finish);
|
|
159
|
+
this.addEventListener('abort', () => {
|
|
160
|
+
const s = this.__sentoriSpan;
|
|
161
|
+
if (!s) return;
|
|
162
|
+
this.__sentoriSpan = undefined;
|
|
163
|
+
s.finish({ status: 'cancelled' });
|
|
164
|
+
addBreadcrumb({
|
|
165
|
+
type: 'net',
|
|
166
|
+
data: { method, url, status: 0, durationMs: Date.now() - (this.__sentoriStart ?? Date.now()), error: 'aborted' },
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return originalSend.call(this, body);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
73
174
|
function mergeHeaders(input: RequestInfo | URL, init?: RequestInit): Headers {
|
|
74
175
|
const out = new Headers();
|
|
75
176
|
if (typeof input !== 'string' && !(input instanceof URL)) {
|