@miradorlabs/parallax-web 2.0.1 → 2.2.0
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/.claude/settings.local.json +8 -2
- package/CLAUDE.md +56 -0
- package/README.md +106 -49
- package/dist/index.d.ts +103 -36
- package/dist/index.esm.js +1650 -415
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +1650 -415
- package/dist/index.umd.js.map +1 -1
- package/example/README.md +1 -1
- package/example/app.js +3 -3
- package/example/proxy-server.js +1 -1
- package/package.json +2 -2
- package/src/parallax/client.ts +32 -29
- package/src/parallax/index.ts +2 -1
- package/src/parallax/trace.ts +268 -65
- package/src/parallax/types.ts +21 -3
- package/tests/parallax.test.ts +534 -174
package/tests/parallax.test.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// ParallaxClient and ParallaxTrace Unit Tests
|
|
2
2
|
import { ParallaxClient, ParallaxTrace } from '../src/parallax';
|
|
3
3
|
import { ParallaxGatewayServiceClient } from 'mirador-gateway-parallax-web/proto/gateway/parallax/v1/Parallax_gatewayServiceClientPb';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CreateTraceRequest,
|
|
6
|
+
UpdateTraceRequest,
|
|
7
|
+
Chain,
|
|
8
|
+
} from 'mirador-gateway-parallax-web/proto/gateway/parallax/v1/parallax_gateway_pb';
|
|
9
|
+
import { ResponseStatus } from 'mirador-gateway-parallax-web/proto/common/v1/status_pb';
|
|
5
10
|
|
|
6
11
|
// Mock the gRPC-Web client
|
|
7
12
|
jest.mock('mirador-gateway-parallax-web/proto/gateway/parallax/v1/Parallax_gatewayServiceClientPb');
|
|
8
13
|
|
|
9
14
|
// Mock console to avoid cluttering test output
|
|
10
15
|
const mockConsoleError = jest.spyOn(console, 'error').mockImplementation();
|
|
11
|
-
const mockConsoleDebug = jest.spyOn(console, 'debug').mockImplementation();
|
|
12
|
-
const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation();
|
|
13
16
|
|
|
14
17
|
// Mock fetch for IP lookup
|
|
15
18
|
global.fetch = jest.fn();
|
|
@@ -46,33 +49,43 @@ beforeAll(() => {
|
|
|
46
49
|
configurable: true,
|
|
47
50
|
});
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Helper to mock Intl.DateTimeFormat (needs to be called after jest.useFakeTimers)
|
|
55
|
+
function mockIntlDateTimeFormat() {
|
|
50
56
|
const mockDateTimeFormat = {
|
|
51
57
|
resolvedOptions: () => ({ timeZone: 'America/New_York' }),
|
|
52
58
|
};
|
|
53
59
|
jest.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => mockDateTimeFormat as unknown as Intl.DateTimeFormat);
|
|
54
|
-
}
|
|
60
|
+
}
|
|
55
61
|
|
|
56
62
|
afterAll(() => {
|
|
57
63
|
mockConsoleError.mockRestore();
|
|
58
|
-
mockConsoleDebug.mockRestore();
|
|
59
|
-
mockConsoleLog.mockRestore();
|
|
60
64
|
});
|
|
61
65
|
|
|
62
66
|
describe('ParallaxClient', () => {
|
|
63
67
|
let mockCreateTrace: jest.Mock;
|
|
68
|
+
let mockUpdateTrace: jest.Mock;
|
|
64
69
|
|
|
65
70
|
beforeEach(() => {
|
|
66
71
|
jest.clearAllMocks();
|
|
72
|
+
jest.useFakeTimers();
|
|
73
|
+
mockIntlDateTimeFormat();
|
|
67
74
|
|
|
68
75
|
// Setup mock for createTrace
|
|
69
76
|
mockCreateTrace = jest.fn().mockResolvedValue({
|
|
70
77
|
getTraceId: () => 'trace-123',
|
|
71
|
-
getStatus: () => ({ getCode: () =>
|
|
78
|
+
getStatus: () => ({ getCode: () => ResponseStatus.StatusCode.STATUS_CODE_SUCCESS }),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Setup mock for updateTrace
|
|
82
|
+
mockUpdateTrace = jest.fn().mockResolvedValue({
|
|
83
|
+
getStatus: () => ({ getCode: () => ResponseStatus.StatusCode.STATUS_CODE_SUCCESS }),
|
|
72
84
|
});
|
|
73
85
|
|
|
74
86
|
(ParallaxGatewayServiceClient as jest.Mock).mockImplementation(() => ({
|
|
75
87
|
createTrace: mockCreateTrace,
|
|
88
|
+
updateTrace: mockUpdateTrace,
|
|
76
89
|
}));
|
|
77
90
|
|
|
78
91
|
// Mock successful IP fetch
|
|
@@ -83,9 +96,8 @@ describe('ParallaxClient', () => {
|
|
|
83
96
|
});
|
|
84
97
|
|
|
85
98
|
afterEach(() => {
|
|
99
|
+
jest.useRealTimers();
|
|
86
100
|
mockConsoleError.mockClear();
|
|
87
|
-
mockConsoleDebug.mockClear();
|
|
88
|
-
mockConsoleLog.mockClear();
|
|
89
101
|
});
|
|
90
102
|
|
|
91
103
|
describe('constructor', () => {
|
|
@@ -97,12 +109,12 @@ describe('ParallaxClient', () => {
|
|
|
97
109
|
|
|
98
110
|
it('should use default gateway URL when not provided', () => {
|
|
99
111
|
const client = new ParallaxClient('my-api-key');
|
|
100
|
-
expect(client.apiUrl).toBe('https://parallax-gateway
|
|
112
|
+
expect(client.apiUrl).toBe('https://parallax-gateway-dev.mirador.org:443');
|
|
101
113
|
});
|
|
102
114
|
|
|
103
115
|
it('should use custom gateway URL when provided', () => {
|
|
104
116
|
const customUrl = 'https://custom-gateway.example.com:443';
|
|
105
|
-
const client = new ParallaxClient('my-api-key', customUrl);
|
|
117
|
+
const client = new ParallaxClient('my-api-key', { apiUrl: customUrl });
|
|
106
118
|
expect(client.apiUrl).toBe(customUrl);
|
|
107
119
|
});
|
|
108
120
|
|
|
@@ -111,18 +123,7 @@ describe('ParallaxClient', () => {
|
|
|
111
123
|
new ParallaxClient(apiKey);
|
|
112
124
|
|
|
113
125
|
expect(ParallaxGatewayServiceClient).toHaveBeenCalledWith(
|
|
114
|
-
'https://parallax-gateway
|
|
115
|
-
{ 'x-parallax-api-key': apiKey }
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should initialize gRPC client with custom URL and credentials', () => {
|
|
120
|
-
const apiKey = 'test-key';
|
|
121
|
-
const customUrl = 'https://custom.example.com:443';
|
|
122
|
-
new ParallaxClient(apiKey, customUrl);
|
|
123
|
-
|
|
124
|
-
expect(ParallaxGatewayServiceClient).toHaveBeenCalledWith(
|
|
125
|
-
customUrl,
|
|
126
|
+
'https://parallax-gateway-dev.mirador.org:443',
|
|
126
127
|
{ 'x-parallax-api-key': apiKey }
|
|
127
128
|
);
|
|
128
129
|
});
|
|
@@ -131,22 +132,26 @@ describe('ParallaxClient', () => {
|
|
|
131
132
|
describe('trace()', () => {
|
|
132
133
|
it('should return a ParallaxTrace instance', () => {
|
|
133
134
|
const client = new ParallaxClient('test-key');
|
|
134
|
-
const trace = client.trace('TestTrace');
|
|
135
|
+
const trace = client.trace({ name: 'TestTrace' });
|
|
135
136
|
|
|
136
137
|
expect(trace).toBeInstanceOf(ParallaxTrace);
|
|
137
138
|
});
|
|
138
139
|
|
|
139
|
-
it('should
|
|
140
|
+
it('should work without options', () => {
|
|
140
141
|
const client = new ParallaxClient('test-key');
|
|
141
|
-
const trace = client.trace(
|
|
142
|
+
const trace = client.trace();
|
|
142
143
|
|
|
143
|
-
// The name is stored internally, we can verify by submitting
|
|
144
144
|
expect(trace).toBeInstanceOf(ParallaxTrace);
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
it('should
|
|
147
|
+
it('should accept all trace options', () => {
|
|
148
148
|
const client = new ParallaxClient('test-key');
|
|
149
|
-
const trace = client.trace(
|
|
149
|
+
const trace = client.trace({
|
|
150
|
+
name: 'TestTrace',
|
|
151
|
+
autoFlush: false,
|
|
152
|
+
flushPeriodMs: 100,
|
|
153
|
+
includeClientMeta: false,
|
|
154
|
+
});
|
|
150
155
|
|
|
151
156
|
expect(trace).toBeInstanceOf(ParallaxTrace);
|
|
152
157
|
});
|
|
@@ -156,17 +161,25 @@ describe('ParallaxClient', () => {
|
|
|
156
161
|
describe('ParallaxTrace', () => {
|
|
157
162
|
let client: ParallaxClient;
|
|
158
163
|
let mockCreateTrace: jest.Mock;
|
|
164
|
+
let mockUpdateTrace: jest.Mock;
|
|
159
165
|
|
|
160
166
|
beforeEach(() => {
|
|
161
167
|
jest.clearAllMocks();
|
|
168
|
+
jest.useFakeTimers();
|
|
169
|
+
mockIntlDateTimeFormat();
|
|
162
170
|
|
|
163
171
|
mockCreateTrace = jest.fn().mockResolvedValue({
|
|
164
172
|
getTraceId: () => 'trace-456',
|
|
165
|
-
getStatus: () => ({ getCode: () =>
|
|
173
|
+
getStatus: () => ({ getCode: () => ResponseStatus.StatusCode.STATUS_CODE_SUCCESS }),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
mockUpdateTrace = jest.fn().mockResolvedValue({
|
|
177
|
+
getStatus: () => ({ getCode: () => ResponseStatus.StatusCode.STATUS_CODE_SUCCESS }),
|
|
166
178
|
});
|
|
167
179
|
|
|
168
180
|
(ParallaxGatewayServiceClient as jest.Mock).mockImplementation(() => ({
|
|
169
181
|
createTrace: mockCreateTrace,
|
|
182
|
+
updateTrace: mockUpdateTrace,
|
|
170
183
|
}));
|
|
171
184
|
|
|
172
185
|
(global.fetch as jest.Mock).mockResolvedValue({
|
|
@@ -177,97 +190,69 @@ describe('ParallaxTrace', () => {
|
|
|
177
190
|
client = new ParallaxClient('test-api-key');
|
|
178
191
|
});
|
|
179
192
|
|
|
193
|
+
afterEach(() => {
|
|
194
|
+
jest.useRealTimers();
|
|
195
|
+
});
|
|
196
|
+
|
|
180
197
|
describe('builder methods', () => {
|
|
181
198
|
it('addAttribute() should return this for chaining', () => {
|
|
182
|
-
const trace = client.trace('TestTrace');
|
|
199
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false });
|
|
183
200
|
const result = trace.addAttribute('key', 'value');
|
|
184
201
|
|
|
185
202
|
expect(result).toBe(trace);
|
|
186
203
|
});
|
|
187
204
|
|
|
188
205
|
it('addAttributes() should return this for chaining', () => {
|
|
189
|
-
const trace = client.trace('TestTrace');
|
|
206
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false });
|
|
190
207
|
const result = trace.addAttributes({ key1: 'value1', key2: 'value2' });
|
|
191
208
|
|
|
192
209
|
expect(result).toBe(trace);
|
|
193
210
|
});
|
|
194
211
|
|
|
195
212
|
it('addTag() should return this for chaining', () => {
|
|
196
|
-
const trace = client.trace('TestTrace');
|
|
213
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false });
|
|
197
214
|
const result = trace.addTag('tag1');
|
|
198
215
|
|
|
199
216
|
expect(result).toBe(trace);
|
|
200
217
|
});
|
|
201
218
|
|
|
202
219
|
it('addTags() should return this for chaining', () => {
|
|
203
|
-
const trace = client.trace('TestTrace');
|
|
220
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false });
|
|
204
221
|
const result = trace.addTags(['tag1', 'tag2']);
|
|
205
222
|
|
|
206
223
|
expect(result).toBe(trace);
|
|
207
224
|
});
|
|
208
225
|
|
|
209
226
|
it('addEvent() should return this for chaining', () => {
|
|
210
|
-
const trace = client.trace('TestTrace');
|
|
227
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false });
|
|
211
228
|
const result = trace.addEvent('event_name');
|
|
212
229
|
|
|
213
230
|
expect(result).toBe(trace);
|
|
214
231
|
});
|
|
215
232
|
|
|
216
233
|
it('addEvent() should accept object details', () => {
|
|
217
|
-
const trace = client.trace('TestTrace');
|
|
234
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false });
|
|
218
235
|
const result = trace.addEvent('event_name', { key: 'value' });
|
|
219
236
|
|
|
220
237
|
expect(result).toBe(trace);
|
|
221
238
|
});
|
|
222
239
|
|
|
223
|
-
it('
|
|
224
|
-
const trace = client.trace('TestTrace');
|
|
225
|
-
const result = trace.
|
|
240
|
+
it('addTxHint() should return this for chaining', () => {
|
|
241
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false });
|
|
242
|
+
const result = trace.addTxHint('0x123', 'ethereum');
|
|
226
243
|
|
|
227
244
|
expect(result).toBe(trace);
|
|
228
245
|
});
|
|
229
246
|
|
|
230
247
|
it('addAttribute() should accept and stringify objects', () => {
|
|
231
|
-
const trace = client.trace('TestTrace');
|
|
248
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false });
|
|
232
249
|
const result = trace.addAttribute('data', { nested: 'value', count: 42 });
|
|
233
250
|
|
|
234
251
|
expect(result).toBe(trace);
|
|
235
252
|
});
|
|
236
253
|
|
|
237
|
-
it('addAttributes() should accept and stringify objects', () => {
|
|
238
|
-
const trace = client.trace('TestTrace');
|
|
239
|
-
const result = trace.addAttributes({
|
|
240
|
-
simple: 'string',
|
|
241
|
-
complex: { nested: true },
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
expect(result).toBe(trace);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe('optional name', () => {
|
|
249
|
-
it('should allow trace without name', async () => {
|
|
250
|
-
await client.trace()
|
|
251
|
-
.addAttribute('key', 'value')
|
|
252
|
-
.create();
|
|
253
|
-
|
|
254
|
-
const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
|
|
255
|
-
expect(request.getName()).toBe('');
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('should allow empty string as name', async () => {
|
|
259
|
-
await client.trace('')
|
|
260
|
-
.addAttribute('key', 'value')
|
|
261
|
-
.create();
|
|
262
|
-
|
|
263
|
-
const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
|
|
264
|
-
expect(request.getName()).toBe('');
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
describe('method chaining', () => {
|
|
269
254
|
it('should support fluent API pattern', () => {
|
|
270
|
-
const trace = client.trace('TestTrace')
|
|
255
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false })
|
|
271
256
|
.addAttribute('from', '0xabc')
|
|
272
257
|
.addAttribute('to', '0xdef')
|
|
273
258
|
.addAttributes({ value: '100', gas: '21000' })
|
|
@@ -275,188 +260,563 @@ describe('ParallaxTrace', () => {
|
|
|
275
260
|
.addTags(['ethereum', 'send'])
|
|
276
261
|
.addEvent('started')
|
|
277
262
|
.addEvent('completed', { success: true })
|
|
278
|
-
.
|
|
263
|
+
.addTxHint('0x123', 'ethereum');
|
|
279
264
|
|
|
280
265
|
expect(trace).toBeInstanceOf(ParallaxTrace);
|
|
281
266
|
});
|
|
282
267
|
});
|
|
283
268
|
|
|
284
|
-
describe('
|
|
285
|
-
it('should
|
|
286
|
-
|
|
287
|
-
.addAttribute('
|
|
288
|
-
.addAttribute('key2', 'value2')
|
|
289
|
-
.create();
|
|
269
|
+
describe('flush() - manual mode', () => {
|
|
270
|
+
it('should call CreateTrace on first flush', async () => {
|
|
271
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false })
|
|
272
|
+
.addAttribute('key', 'value');
|
|
290
273
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
expect(request.getName()).toBe('TestTrace');
|
|
274
|
+
trace.flush();
|
|
275
|
+
await jest.runAllTimersAsync();
|
|
294
276
|
|
|
295
|
-
|
|
296
|
-
expect(
|
|
297
|
-
expect(attrsMap.get('key2')).toBe('value2');
|
|
277
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
278
|
+
expect(mockUpdateTrace).not.toHaveBeenCalled();
|
|
298
279
|
});
|
|
299
280
|
|
|
300
|
-
it('should
|
|
301
|
-
|
|
302
|
-
.addAttribute('
|
|
303
|
-
|
|
304
|
-
|
|
281
|
+
it('should call UpdateTrace on subsequent flushes', async () => {
|
|
282
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false })
|
|
283
|
+
.addAttribute('key', 'value');
|
|
284
|
+
|
|
285
|
+
trace.flush();
|
|
286
|
+
await jest.runAllTimersAsync();
|
|
287
|
+
|
|
288
|
+
trace.addEvent('new_event');
|
|
289
|
+
trace.flush();
|
|
290
|
+
await jest.runAllTimersAsync();
|
|
291
|
+
|
|
292
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
293
|
+
expect(mockUpdateTrace).toHaveBeenCalledTimes(1);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should set trace name in CreateTraceRequest', async () => {
|
|
297
|
+
const trace = client.trace({ name: 'MyTraceName', autoFlush: false })
|
|
298
|
+
.addAttribute('key', 'value');
|
|
299
|
+
|
|
300
|
+
trace.flush();
|
|
301
|
+
await jest.runAllTimersAsync();
|
|
305
302
|
|
|
306
303
|
const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
|
|
307
|
-
|
|
304
|
+
expect(request.getName()).toBe('MyTraceName');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should include TraceData with attributes', async () => {
|
|
308
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false, includeClientMeta: false })
|
|
309
|
+
.addAttribute('key1', 'value1')
|
|
310
|
+
.addAttribute('key2', 'value2');
|
|
311
|
+
|
|
312
|
+
trace.flush();
|
|
313
|
+
await jest.runAllTimersAsync();
|
|
314
|
+
|
|
315
|
+
const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
|
|
316
|
+
const data = request.getData();
|
|
317
|
+
expect(data).toBeDefined();
|
|
308
318
|
|
|
309
|
-
|
|
310
|
-
expect(
|
|
311
|
-
|
|
319
|
+
const attrsList = data!.getAttributesList();
|
|
320
|
+
expect(attrsList.length).toBe(1);
|
|
321
|
+
|
|
322
|
+
const attrsMap = attrsList[0].getAttributesMap();
|
|
323
|
+
expect(attrsMap.get('key1')).toBe('value1');
|
|
324
|
+
expect(attrsMap.get('key2')).toBe('value2');
|
|
312
325
|
});
|
|
313
326
|
|
|
314
|
-
it('should include tags in
|
|
315
|
-
|
|
316
|
-
.addTags(['tag1', 'tag2', 'tag3'])
|
|
317
|
-
|
|
327
|
+
it('should include tags in TraceData', async () => {
|
|
328
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false, includeClientMeta: false })
|
|
329
|
+
.addTags(['tag1', 'tag2', 'tag3']);
|
|
330
|
+
|
|
331
|
+
trace.flush();
|
|
332
|
+
await jest.runAllTimersAsync();
|
|
318
333
|
|
|
319
334
|
const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
|
|
320
|
-
|
|
335
|
+
const data = request.getData();
|
|
336
|
+
const tagsList = data!.getTagsList();
|
|
337
|
+
expect(tagsList.length).toBe(1);
|
|
338
|
+
expect(tagsList[0].getTagsList()).toEqual(['tag1', 'tag2', 'tag3']);
|
|
321
339
|
});
|
|
322
340
|
|
|
323
|
-
it('should include events in
|
|
324
|
-
|
|
341
|
+
it('should include events in TraceData', async () => {
|
|
342
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false, includeClientMeta: false })
|
|
325
343
|
.addEvent('event1', 'details1')
|
|
326
|
-
.addEvent('event2', { key: 'value' })
|
|
327
|
-
|
|
344
|
+
.addEvent('event2', { key: 'value' });
|
|
345
|
+
|
|
346
|
+
trace.flush();
|
|
347
|
+
await jest.runAllTimersAsync();
|
|
328
348
|
|
|
329
349
|
const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
|
|
330
|
-
const
|
|
350
|
+
const data = request.getData();
|
|
351
|
+
const events = data!.getEventsList();
|
|
331
352
|
expect(events.length).toBe(2);
|
|
332
353
|
expect(events[0].getName()).toBe('event1');
|
|
333
354
|
expect(events[1].getName()).toBe('event2');
|
|
334
355
|
});
|
|
335
356
|
|
|
336
|
-
it('should include
|
|
337
|
-
|
|
338
|
-
.
|
|
339
|
-
.
|
|
357
|
+
it('should include multiple txHashHints in TraceData', async () => {
|
|
358
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false, includeClientMeta: false })
|
|
359
|
+
.addTxHint('0xabc123', 'ethereum', 'first tx')
|
|
360
|
+
.addTxHint('0xdef456', 'polygon', 'second tx');
|
|
340
361
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
expect(txHint).toBeDefined();
|
|
344
|
-
expect(txHint?.getTxHash()).toBe('0xabc123');
|
|
345
|
-
expect(txHint?.getChain()).toBe(Chain.CHAIN_ETHEREUM);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it('should support different chain names', async () => {
|
|
349
|
-
await client.trace('TestTrace')
|
|
350
|
-
.setTxHint('0xabc123', 'polygon')
|
|
351
|
-
.create();
|
|
362
|
+
trace.flush();
|
|
363
|
+
await jest.runAllTimersAsync();
|
|
352
364
|
|
|
353
365
|
const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
|
|
354
|
-
const
|
|
355
|
-
|
|
366
|
+
const data = request.getData();
|
|
367
|
+
const hints = data!.getTxHashHintsList();
|
|
368
|
+
expect(hints.length).toBe(2);
|
|
369
|
+
expect(hints[0].getTxHash()).toBe('0xabc123');
|
|
370
|
+
expect(hints[0].getChain()).toBe(Chain.CHAIN_ETHEREUM);
|
|
371
|
+
expect(hints[1].getTxHash()).toBe('0xdef456');
|
|
372
|
+
expect(hints[1].getChain()).toBe(Chain.CHAIN_POLYGON);
|
|
356
373
|
});
|
|
357
374
|
|
|
358
375
|
it('should include client metadata by default', async () => {
|
|
359
|
-
|
|
360
|
-
.addAttribute('custom', 'value')
|
|
361
|
-
|
|
376
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false })
|
|
377
|
+
.addAttribute('custom', 'value');
|
|
378
|
+
|
|
379
|
+
trace.flush();
|
|
380
|
+
await jest.runAllTimersAsync();
|
|
362
381
|
|
|
363
382
|
const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
|
|
364
|
-
const
|
|
383
|
+
const data = request.getData();
|
|
384
|
+
const attrsList = data!.getAttributesList();
|
|
385
|
+
expect(attrsList.length).toBe(1);
|
|
365
386
|
|
|
366
|
-
|
|
387
|
+
const attrsMap = attrsList[0].getAttributesMap();
|
|
367
388
|
expect(attrsMap.get('custom')).toBe('value');
|
|
368
|
-
|
|
369
|
-
// Client metadata should be prefixed with 'client.'
|
|
370
389
|
expect(attrsMap.get('client.browser')).toBe('Chrome');
|
|
371
390
|
expect(attrsMap.get('client.os')).toBe('macOS');
|
|
372
391
|
});
|
|
373
392
|
|
|
374
|
-
it('should not include client metadata when
|
|
375
|
-
|
|
376
|
-
.addAttribute('custom', 'value')
|
|
377
|
-
|
|
393
|
+
it('should not include client metadata when disabled', async () => {
|
|
394
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false, includeClientMeta: false })
|
|
395
|
+
.addAttribute('custom', 'value');
|
|
396
|
+
|
|
397
|
+
trace.flush();
|
|
398
|
+
await jest.runAllTimersAsync();
|
|
378
399
|
|
|
379
400
|
const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
|
|
380
|
-
const
|
|
401
|
+
const data = request.getData();
|
|
402
|
+
const attrsList = data!.getAttributesList();
|
|
403
|
+
const attrsMap = attrsList[0].getAttributesMap();
|
|
381
404
|
|
|
382
405
|
expect(attrsMap.get('custom')).toBe('value');
|
|
383
406
|
expect(attrsMap.get('client.browser')).toBeUndefined();
|
|
384
407
|
});
|
|
385
408
|
|
|
386
|
-
it('should
|
|
387
|
-
const
|
|
409
|
+
it('should set traceId after successful CreateTrace', async () => {
|
|
410
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false })
|
|
411
|
+
.addAttribute('key', 'value');
|
|
412
|
+
|
|
413
|
+
expect(trace.getTraceId()).toBeNull();
|
|
414
|
+
|
|
415
|
+
trace.flush();
|
|
416
|
+
await jest.runAllTimersAsync();
|
|
417
|
+
|
|
418
|
+
expect(trace.getTraceId()).toBe('trace-456');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('should maintain strict ordering of flushes', async () => {
|
|
422
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false })
|
|
423
|
+
.addAttribute('first', 'value');
|
|
424
|
+
|
|
425
|
+
trace.flush();
|
|
426
|
+
trace.addEvent('second');
|
|
427
|
+
trace.flush();
|
|
428
|
+
trace.addEvent('third');
|
|
429
|
+
trace.flush();
|
|
430
|
+
|
|
431
|
+
await jest.runAllTimersAsync();
|
|
432
|
+
|
|
433
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
434
|
+
expect(mockUpdateTrace).toHaveBeenCalledTimes(2);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe('auto-flush mode', () => {
|
|
439
|
+
it('should auto-flush after flushPeriodMs of inactivity', async () => {
|
|
440
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: true, flushPeriodMs: 50, includeClientMeta: false })
|
|
441
|
+
.addAttribute('key', 'value');
|
|
442
|
+
|
|
443
|
+
expect(mockCreateTrace).not.toHaveBeenCalled();
|
|
444
|
+
|
|
445
|
+
// Advance time past flushPeriodMs
|
|
446
|
+
jest.advanceTimersByTime(50);
|
|
447
|
+
await jest.runAllTimersAsync();
|
|
448
|
+
|
|
449
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
450
|
+
expect(trace.getTraceId()).toBe('trace-456');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should reset timer on each SDK call', async () => {
|
|
454
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: true, flushPeriodMs: 50, includeClientMeta: false });
|
|
455
|
+
|
|
456
|
+
trace.addAttribute('key1', 'value1');
|
|
457
|
+
jest.advanceTimersByTime(30);
|
|
458
|
+
|
|
459
|
+
trace.addAttribute('key2', 'value2');
|
|
460
|
+
jest.advanceTimersByTime(30);
|
|
461
|
+
|
|
462
|
+
// Should not have flushed yet (timer reset)
|
|
463
|
+
expect(mockCreateTrace).not.toHaveBeenCalled();
|
|
464
|
+
|
|
465
|
+
jest.advanceTimersByTime(50);
|
|
466
|
+
await jest.runAllTimersAsync();
|
|
467
|
+
|
|
468
|
+
// Now should have flushed with both attributes
|
|
469
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should allow explicit flush() even when autoFlush is true', async () => {
|
|
473
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: true, flushPeriodMs: 50, includeClientMeta: false })
|
|
474
|
+
.addAttribute('key', 'value');
|
|
475
|
+
|
|
476
|
+
// Explicit flush before timer expires
|
|
477
|
+
trace.flush();
|
|
478
|
+
await jest.runAllTimersAsync();
|
|
479
|
+
|
|
480
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
481
|
+
|
|
482
|
+
// Timer should be cancelled, no duplicate flush
|
|
483
|
+
jest.advanceTimersByTime(100);
|
|
484
|
+
await jest.runAllTimersAsync();
|
|
485
|
+
|
|
486
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should call UpdateTrace on subsequent auto-flushes', async () => {
|
|
490
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: true, flushPeriodMs: 50, includeClientMeta: false })
|
|
491
|
+
.addAttribute('key', 'value');
|
|
492
|
+
|
|
493
|
+
jest.advanceTimersByTime(50);
|
|
494
|
+
await jest.runAllTimersAsync();
|
|
495
|
+
|
|
496
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
497
|
+
|
|
498
|
+
trace.addEvent('new_event');
|
|
499
|
+
jest.advanceTimersByTime(50);
|
|
500
|
+
await jest.runAllTimersAsync();
|
|
388
501
|
|
|
389
|
-
expect(
|
|
502
|
+
expect(mockUpdateTrace).toHaveBeenCalledTimes(1);
|
|
390
503
|
});
|
|
504
|
+
});
|
|
391
505
|
|
|
392
|
-
|
|
506
|
+
describe('immediate flush mode (flushPeriodMs: 0)', () => {
|
|
507
|
+
it('should flush immediately on every SDK call', async () => {
|
|
508
|
+
const trace = client.trace({ name: 'TestTrace', flushPeriodMs: 0, includeClientMeta: false });
|
|
509
|
+
|
|
510
|
+
trace.addAttribute('key1', 'value1');
|
|
511
|
+
await jest.runAllTimersAsync();
|
|
512
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
513
|
+
|
|
514
|
+
trace.addAttribute('key2', 'value2');
|
|
515
|
+
await jest.runAllTimersAsync();
|
|
516
|
+
expect(mockUpdateTrace).toHaveBeenCalledTimes(1);
|
|
517
|
+
|
|
518
|
+
trace.addEvent('event1');
|
|
519
|
+
await jest.runAllTimersAsync();
|
|
520
|
+
expect(mockUpdateTrace).toHaveBeenCalledTimes(2);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('should send each SDK call as a separate request', async () => {
|
|
524
|
+
const trace = client.trace({ name: 'TestTrace', flushPeriodMs: 0, includeClientMeta: false });
|
|
525
|
+
|
|
526
|
+
trace.addAttribute('first', 'value');
|
|
527
|
+
await jest.runAllTimersAsync();
|
|
528
|
+
|
|
529
|
+
trace.addTxHint('0xabc', 'ethereum');
|
|
530
|
+
await jest.runAllTimersAsync();
|
|
531
|
+
|
|
532
|
+
trace.addEvent('done');
|
|
533
|
+
await jest.runAllTimersAsync();
|
|
534
|
+
|
|
535
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
536
|
+
expect(mockUpdateTrace).toHaveBeenCalledTimes(2);
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
describe('error handling', () => {
|
|
541
|
+
it('should log error on CreateTrace failure after retries', async () => {
|
|
393
542
|
mockCreateTrace.mockRejectedValue(new Error('Connection failed'));
|
|
394
543
|
|
|
395
|
-
|
|
544
|
+
// Disable retries for faster test
|
|
545
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false, maxRetries: 0 })
|
|
546
|
+
.addAttribute('key', 'value');
|
|
547
|
+
|
|
548
|
+
trace.flush();
|
|
549
|
+
await jest.runAllTimersAsync();
|
|
550
|
+
|
|
551
|
+
expect(mockConsoleError).toHaveBeenCalledWith(
|
|
552
|
+
'[ParallaxTrace] CreateTrace error after retries:',
|
|
553
|
+
expect.any(Error)
|
|
554
|
+
);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('should log error on UpdateTrace failure after retries', async () => {
|
|
558
|
+
mockUpdateTrace.mockRejectedValue(new Error('Connection failed'));
|
|
559
|
+
|
|
560
|
+
// Disable retries for faster test
|
|
561
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false, maxRetries: 0 })
|
|
562
|
+
.addAttribute('key', 'value');
|
|
563
|
+
|
|
564
|
+
trace.flush();
|
|
565
|
+
await jest.runAllTimersAsync();
|
|
396
566
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
567
|
+
trace.addEvent('event');
|
|
568
|
+
trace.flush();
|
|
569
|
+
await jest.runAllTimersAsync();
|
|
570
|
+
|
|
571
|
+
expect(mockConsoleError).toHaveBeenCalledWith(
|
|
572
|
+
'[ParallaxTrace] UpdateTrace error after retries:',
|
|
400
573
|
expect.any(Error)
|
|
401
574
|
);
|
|
402
575
|
});
|
|
403
576
|
|
|
404
|
-
it('should
|
|
577
|
+
it('should log error on status failure', async () => {
|
|
405
578
|
mockCreateTrace.mockResolvedValue({
|
|
406
579
|
getTraceId: () => '',
|
|
407
580
|
getStatus: () => ({
|
|
408
|
-
getCode: () =>
|
|
409
|
-
getErrorMessage: () => '
|
|
581
|
+
getCode: () => ResponseStatus.StatusCode.STATUS_CODE_VALIDATION_ERROR,
|
|
582
|
+
getErrorMessage: () => 'Validation error',
|
|
410
583
|
}),
|
|
411
584
|
});
|
|
412
585
|
|
|
413
|
-
const
|
|
586
|
+
const trace = client.trace({ name: 'TestTrace', autoFlush: false })
|
|
587
|
+
.addAttribute('key', 'value');
|
|
588
|
+
|
|
589
|
+
trace.flush();
|
|
590
|
+
await jest.runAllTimersAsync();
|
|
414
591
|
|
|
415
|
-
expect(
|
|
416
|
-
|
|
417
|
-
'
|
|
418
|
-
'Internal server error'
|
|
592
|
+
expect(mockConsoleError).toHaveBeenCalledWith(
|
|
593
|
+
'[ParallaxTrace] CreateTrace failed:',
|
|
594
|
+
'Validation error'
|
|
419
595
|
);
|
|
420
596
|
});
|
|
597
|
+
});
|
|
421
598
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
.addAttribute('key', 'value')
|
|
425
|
-
.create();
|
|
599
|
+
describe('retry behavior', () => {
|
|
600
|
+
let mockConsoleWarn: jest.SpyInstance;
|
|
426
601
|
|
|
427
|
-
|
|
428
|
-
|
|
602
|
+
beforeEach(() => {
|
|
603
|
+
mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
afterEach(() => {
|
|
607
|
+
mockConsoleWarn.mockRestore();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('should retry on CreateTrace failure with exponential backoff', async () => {
|
|
611
|
+
// Fail twice, then succeed
|
|
612
|
+
mockCreateTrace
|
|
613
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
614
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
615
|
+
.mockResolvedValueOnce({
|
|
616
|
+
getTraceId: () => 'trace-retry-success',
|
|
617
|
+
getStatus: () => ({ getCode: () => ResponseStatus.StatusCode.STATUS_CODE_SUCCESS }),
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
const trace = client.trace({
|
|
621
|
+
name: 'TestTrace',
|
|
622
|
+
autoFlush: false,
|
|
623
|
+
includeClientMeta: false,
|
|
624
|
+
maxRetries: 3,
|
|
625
|
+
retryBackoff: 100,
|
|
626
|
+
}).addAttribute('key', 'value');
|
|
627
|
+
|
|
628
|
+
trace.flush();
|
|
629
|
+
|
|
630
|
+
// First attempt happens immediately
|
|
631
|
+
await jest.advanceTimersByTimeAsync(0);
|
|
632
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
633
|
+
|
|
634
|
+
// Before first retry delay (100ms * 2^0 = 100ms), no second call
|
|
635
|
+
await jest.advanceTimersByTimeAsync(99);
|
|
636
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
637
|
+
|
|
638
|
+
// After first retry delay, second call happens
|
|
639
|
+
await jest.advanceTimersByTimeAsync(1);
|
|
640
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(2);
|
|
641
|
+
|
|
642
|
+
// Before second retry delay (100ms * 2^1 = 200ms), no third call
|
|
643
|
+
await jest.advanceTimersByTimeAsync(199);
|
|
644
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(2);
|
|
645
|
+
|
|
646
|
+
// After second retry delay, third call happens
|
|
647
|
+
await jest.advanceTimersByTimeAsync(1);
|
|
648
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(3);
|
|
649
|
+
|
|
650
|
+
await jest.runAllTimersAsync();
|
|
651
|
+
|
|
652
|
+
expect(trace.getTraceId()).toBe('trace-retry-success');
|
|
653
|
+
expect(mockConsoleWarn).toHaveBeenCalledTimes(2);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('should retry on UpdateTrace failure', async () => {
|
|
657
|
+
// First CreateTrace succeeds
|
|
658
|
+
mockCreateTrace.mockResolvedValue({
|
|
659
|
+
getTraceId: () => 'trace-456',
|
|
660
|
+
getStatus: () => ({ getCode: () => ResponseStatus.StatusCode.STATUS_CODE_SUCCESS }),
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// UpdateTrace fails once, then succeeds
|
|
664
|
+
mockUpdateTrace
|
|
665
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
666
|
+
.mockResolvedValueOnce({
|
|
667
|
+
getStatus: () => ({ getCode: () => ResponseStatus.StatusCode.STATUS_CODE_SUCCESS }),
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const trace = client.trace({
|
|
671
|
+
name: 'TestTrace',
|
|
672
|
+
autoFlush: false,
|
|
673
|
+
includeClientMeta: false,
|
|
674
|
+
maxRetries: 2,
|
|
675
|
+
retryBackoff: 50,
|
|
676
|
+
}).addAttribute('key', 'value');
|
|
677
|
+
|
|
678
|
+
trace.flush();
|
|
679
|
+
await jest.runAllTimersAsync();
|
|
680
|
+
|
|
681
|
+
trace.addEvent('event');
|
|
682
|
+
trace.flush();
|
|
683
|
+
|
|
684
|
+
// Wait for retry
|
|
685
|
+
await jest.advanceTimersByTimeAsync(50);
|
|
686
|
+
await jest.runAllTimersAsync();
|
|
687
|
+
|
|
688
|
+
expect(mockUpdateTrace).toHaveBeenCalledTimes(2);
|
|
689
|
+
expect(mockConsoleWarn).toHaveBeenCalledTimes(1);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('should respect maxRetries option', async () => {
|
|
693
|
+
mockCreateTrace.mockRejectedValue(new Error('Always fails'));
|
|
694
|
+
|
|
695
|
+
const trace = client.trace({
|
|
696
|
+
name: 'TestTrace',
|
|
697
|
+
autoFlush: false,
|
|
698
|
+
includeClientMeta: false,
|
|
699
|
+
maxRetries: 2,
|
|
700
|
+
retryBackoff: 10,
|
|
701
|
+
}).addAttribute('key', 'value');
|
|
702
|
+
|
|
703
|
+
trace.flush();
|
|
704
|
+
|
|
705
|
+
// Allow all retries to complete
|
|
706
|
+
await jest.advanceTimersByTimeAsync(10); // First retry
|
|
707
|
+
await jest.advanceTimersByTimeAsync(20); // Second retry
|
|
708
|
+
await jest.runAllTimersAsync();
|
|
709
|
+
|
|
710
|
+
// Initial attempt + 2 retries = 3 total calls
|
|
711
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(3);
|
|
712
|
+
expect(mockConsoleError).toHaveBeenCalledWith(
|
|
713
|
+
'[ParallaxTrace] CreateTrace error after retries:',
|
|
714
|
+
expect.any(Error)
|
|
715
|
+
);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should use default retry options', async () => {
|
|
719
|
+
mockCreateTrace
|
|
720
|
+
.mockRejectedValueOnce(new Error('Network error'))
|
|
721
|
+
.mockResolvedValueOnce({
|
|
722
|
+
getTraceId: () => 'trace-default',
|
|
723
|
+
getStatus: () => ({ getCode: () => ResponseStatus.StatusCode.STATUS_CODE_SUCCESS }),
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// Use default options (maxRetries: 3, retryBackoff: 1000)
|
|
727
|
+
const trace = client.trace({
|
|
728
|
+
name: 'TestTrace',
|
|
729
|
+
autoFlush: false,
|
|
730
|
+
includeClientMeta: false,
|
|
731
|
+
}).addAttribute('key', 'value');
|
|
732
|
+
|
|
733
|
+
trace.flush();
|
|
734
|
+
|
|
735
|
+
// First attempt fails
|
|
736
|
+
await jest.advanceTimersByTimeAsync(0);
|
|
737
|
+
|
|
738
|
+
// Wait for first retry (1000ms default backoff)
|
|
739
|
+
await jest.advanceTimersByTimeAsync(1000);
|
|
740
|
+
await jest.runAllTimersAsync();
|
|
741
|
+
|
|
742
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(2);
|
|
743
|
+
expect(trace.getTraceId()).toBe('trace-default');
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it('should disable retries when maxRetries is 0', async () => {
|
|
747
|
+
mockCreateTrace.mockRejectedValue(new Error('Network error'));
|
|
748
|
+
|
|
749
|
+
const trace = client.trace({
|
|
750
|
+
name: 'TestTrace',
|
|
751
|
+
autoFlush: false,
|
|
752
|
+
includeClientMeta: false,
|
|
753
|
+
maxRetries: 0,
|
|
754
|
+
}).addAttribute('key', 'value');
|
|
755
|
+
|
|
756
|
+
trace.flush();
|
|
757
|
+
await jest.runAllTimersAsync();
|
|
758
|
+
|
|
759
|
+
// Only 1 attempt, no retries
|
|
760
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
761
|
+
expect(mockConsoleWarn).not.toHaveBeenCalled();
|
|
429
762
|
});
|
|
430
763
|
});
|
|
431
764
|
|
|
432
765
|
describe('integration test', () => {
|
|
433
|
-
it('should work with real usage pattern', async () => {
|
|
434
|
-
const
|
|
766
|
+
it('should work with real usage pattern - manual flush', async () => {
|
|
767
|
+
const trace = client.trace({ name: 'SendTransaction', autoFlush: false })
|
|
435
768
|
.addAttribute('from', '0xabc123')
|
|
436
769
|
.addAttribute('to', '0xdef456')
|
|
437
770
|
.addAttribute('value', '1.5')
|
|
438
|
-
.addAttribute('network', 'ethereum')
|
|
439
771
|
.addTags(['transaction', 'send', 'ethereum'])
|
|
440
772
|
.addEvent('wallet_connected', { wallet: 'MetaMask' })
|
|
441
|
-
.addEvent('transaction_initiated')
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
773
|
+
.addEvent('transaction_initiated');
|
|
774
|
+
|
|
775
|
+
trace.flush();
|
|
776
|
+
await jest.runAllTimersAsync();
|
|
445
777
|
|
|
446
|
-
expect(traceId).toBe('trace-456');
|
|
447
778
|
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
779
|
+
expect(trace.getTraceId()).toBe('trace-456');
|
|
448
780
|
|
|
449
781
|
const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
|
|
450
782
|
expect(request.getName()).toBe('SendTransaction');
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
expect(
|
|
783
|
+
|
|
784
|
+
// Add more data and flush again
|
|
785
|
+
trace.addEvent('transaction_sent', { blockNumber: 12345 })
|
|
786
|
+
.addTxHint('0xtxhash123', 'ethereum');
|
|
787
|
+
|
|
788
|
+
trace.flush();
|
|
789
|
+
await jest.runAllTimersAsync();
|
|
790
|
+
|
|
791
|
+
expect(mockUpdateTrace).toHaveBeenCalledTimes(1);
|
|
792
|
+
|
|
793
|
+
const updateRequest = mockUpdateTrace.mock.calls[0][0] as UpdateTraceRequest;
|
|
794
|
+
expect(updateRequest.getTraceId()).toBe('trace-456');
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('should work with real usage pattern - auto flush', async () => {
|
|
798
|
+
const trace = client.trace({ name: 'SendTransaction', flushPeriodMs: 50 })
|
|
799
|
+
.addAttribute('from', '0xabc123')
|
|
800
|
+
.addAttribute('to', '0xdef456')
|
|
801
|
+
.addTags(['transaction', 'ethereum'])
|
|
802
|
+
.addEvent('wallet_connected');
|
|
803
|
+
|
|
804
|
+
// Let auto-flush trigger
|
|
805
|
+
jest.advanceTimersByTime(50);
|
|
806
|
+
await jest.runAllTimersAsync();
|
|
807
|
+
|
|
808
|
+
expect(mockCreateTrace).toHaveBeenCalledTimes(1);
|
|
809
|
+
expect(trace.getTraceId()).toBe('trace-456');
|
|
810
|
+
|
|
811
|
+
// Add more data
|
|
812
|
+
trace.addEvent('transaction_sent')
|
|
813
|
+
.addTxHint('0xtxhash', 'ethereum');
|
|
814
|
+
|
|
815
|
+
// Let auto-flush trigger again
|
|
816
|
+
jest.advanceTimersByTime(50);
|
|
817
|
+
await jest.runAllTimersAsync();
|
|
818
|
+
|
|
819
|
+
expect(mockUpdateTrace).toHaveBeenCalledTimes(1);
|
|
460
820
|
});
|
|
461
821
|
});
|
|
462
822
|
});
|