@miradorlabs/parallax-web 1.0.7 → 2.0.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.
@@ -1,49 +1,91 @@
1
- // ParallaxClient Unit Tests
2
- import { ParallaxClient } from '../src/parallax';
3
- import { GrpcWebRpc } from '../src/grpc';
4
- import * as apiGateway from "mirador-gateway-parallax-web/proto/gateway/parallax/v1/parallax_gateway";
5
- import { ResponseStatus_StatusCode } from "mirador-gateway-parallax-web/proto/common/v1/status";
1
+ // ParallaxClient and ParallaxTrace Unit Tests
2
+ import { ParallaxClient, ParallaxTrace } from '../src/parallax';
3
+ import { ParallaxGatewayServiceClient } from 'mirador-gateway-parallax-web/proto/gateway/parallax/v1/Parallax_gatewayServiceClientPb';
4
+ import { CreateTraceRequest, Chain } from 'mirador-gateway-parallax-web/proto/gateway/parallax/v1/parallax_gateway_pb';
6
5
 
7
- // Mock the GrpcWebRpc class
8
- jest.mock('../src/grpc');
6
+ // Mock the gRPC-Web client
7
+ jest.mock('mirador-gateway-parallax-web/proto/gateway/parallax/v1/Parallax_gatewayServiceClientPb');
9
8
 
10
- // Mock console.error to avoid cluttering test output
9
+ // Mock console to avoid cluttering test output
11
10
  const mockConsoleError = jest.spyOn(console, 'error').mockImplementation();
11
+ const mockConsoleDebug = jest.spyOn(console, 'debug').mockImplementation();
12
+ const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation();
13
+
14
+ // Mock fetch for IP lookup
15
+ global.fetch = jest.fn();
16
+
17
+ // Setup browser mock values using Object.defineProperty on navigator
18
+ beforeAll(() => {
19
+ Object.defineProperty(navigator, 'userAgent', {
20
+ value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
21
+ configurable: true,
22
+ });
23
+ Object.defineProperty(navigator, 'platform', {
24
+ value: 'MacIntel',
25
+ configurable: true,
26
+ });
27
+ Object.defineProperty(navigator, 'language', {
28
+ value: 'en-US',
29
+ configurable: true,
30
+ });
31
+
32
+ // Mock window properties
33
+ Object.defineProperty(window, 'innerWidth', { value: 1440, configurable: true });
34
+ Object.defineProperty(window, 'innerHeight', { value: 900, configurable: true });
35
+ Object.defineProperty(window.screen, 'width', { value: 1920, configurable: true });
36
+ Object.defineProperty(window.screen, 'height', { value: 1080, configurable: true });
37
+
38
+ // Mock location - need to delete first in jsdom
39
+ // @ts-expect-error - deleting window.location for test mocking
40
+ delete window.location;
41
+ window.location = { href: 'https://example.com/page' } as unknown as Location;
42
+
43
+ // Mock document.referrer - need to use a getter
44
+ Object.defineProperty(document, 'referrer', {
45
+ get: () => 'https://google.com',
46
+ configurable: true,
47
+ });
48
+
49
+ // Mock Intl.DateTimeFormat
50
+ const mockDateTimeFormat = {
51
+ resolvedOptions: () => ({ timeZone: 'America/New_York' }),
52
+ };
53
+ jest.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => mockDateTimeFormat as unknown as Intl.DateTimeFormat);
54
+ });
55
+
56
+ afterAll(() => {
57
+ mockConsoleError.mockRestore();
58
+ mockConsoleDebug.mockRestore();
59
+ mockConsoleLog.mockRestore();
60
+ });
12
61
 
13
62
  describe('ParallaxClient', () => {
14
- let parallaxClient: ParallaxClient;
15
- let mockApiGatewayClient: jest.Mocked<apiGateway.ParallaxGatewayServiceClientImpl>;
63
+ let mockCreateTrace: jest.Mock;
16
64
 
17
65
  beforeEach(() => {
18
- // Clear all mocks before each test
19
66
  jest.clearAllMocks();
20
67
 
21
- // Create a new ParallaxClient instance
22
- parallaxClient = new ParallaxClient("test-api-key");
23
-
24
- // Create mock for ApiGatewayServiceClientImpl
25
- mockApiGatewayClient = {
26
- CreateTrace: jest.fn(),
27
- StartSpan: jest.fn(),
28
- FinishSpan: jest.fn(),
29
- AddSpanAttributes: jest.fn(),
30
- AddSpanEvent: jest.fn(),
31
- AddSpanError: jest.fn(),
32
- AddSpanHint: jest.fn(),
33
- } as unknown as jest.Mocked<apiGateway.ParallaxGatewayServiceClientImpl>;
34
-
35
- // Mock the ParallaxGatewayServiceClientImpl constructor
36
- jest
37
- .spyOn(apiGateway, "ParallaxGatewayServiceClientImpl")
38
- .mockImplementation(() => mockApiGatewayClient);
68
+ // Setup mock for createTrace
69
+ mockCreateTrace = jest.fn().mockResolvedValue({
70
+ getTraceId: () => 'trace-123',
71
+ getStatus: () => ({ getCode: () => 1 }),
72
+ });
73
+
74
+ (ParallaxGatewayServiceClient as jest.Mock).mockImplementation(() => ({
75
+ createTrace: mockCreateTrace,
76
+ }));
77
+
78
+ // Mock successful IP fetch
79
+ (global.fetch as jest.Mock).mockResolvedValue({
80
+ ok: true,
81
+ json: () => Promise.resolve({ ip: '192.168.1.1' }),
82
+ });
39
83
  });
40
84
 
41
85
  afterEach(() => {
42
86
  mockConsoleError.mockClear();
43
- });
44
-
45
- afterAll(() => {
46
- mockConsoleError.mockRestore();
87
+ mockConsoleDebug.mockClear();
88
+ mockConsoleLog.mockClear();
47
89
  });
48
90
 
49
91
  describe('constructor', () => {
@@ -53,387 +95,368 @@ describe('ParallaxClient', () => {
53
95
  expect(client.apiKey).toBe('my-api-key');
54
96
  });
55
97
 
56
- it('should create a ParallaxClient instance without API key', () => {
57
- const client = new ParallaxClient();
58
- expect(client).toBeInstanceOf(ParallaxClient);
59
- expect(client.apiKey).toBeUndefined();
98
+ it('should use default gateway URL when not provided', () => {
99
+ const client = new ParallaxClient('my-api-key');
100
+ expect(client.apiUrl).toBe('https://parallax-gateway.dev.mirador.org:443');
101
+ });
102
+
103
+ it('should use custom gateway URL when provided', () => {
104
+ const customUrl = 'https://custom-gateway.example.com:443';
105
+ const client = new ParallaxClient('my-api-key', customUrl);
106
+ expect(client.apiUrl).toBe(customUrl);
107
+ });
108
+
109
+ it('should initialize gRPC client with credentials', () => {
110
+ const apiKey = 'test-key';
111
+ new ParallaxClient(apiKey);
112
+
113
+ expect(ParallaxGatewayServiceClient).toHaveBeenCalledWith(
114
+ 'https://parallax-gateway.dev.mirador.org:443',
115
+ { 'x-parallax-api-key': apiKey }
116
+ );
60
117
  });
61
118
 
62
- it('should initialize GrpcWebRpc with the correct URL and API key', () => {
119
+ it('should initialize gRPC client with custom URL and credentials', () => {
63
120
  const apiKey = 'test-key';
64
- const customUrl = 'https://custom-gateway.example.com:50053';
121
+ const customUrl = 'https://custom.example.com:443';
65
122
  new ParallaxClient(apiKey, customUrl);
66
- expect(GrpcWebRpc).toHaveBeenCalledWith(customUrl, apiKey);
123
+
124
+ expect(ParallaxGatewayServiceClient).toHaveBeenCalledWith(
125
+ customUrl,
126
+ { 'x-parallax-api-key': apiKey }
127
+ );
67
128
  });
68
129
  });
69
130
 
70
- describe('createTrace', () => {
71
- it('should create a trace successfully', async () => {
72
- const mockRequest: apiGateway.CreateTraceRequest = {
73
- name: 'Test Trace',
74
- attributes: {
75
- 'project.id': 'test-project',
76
- 'environment': 'test'
77
- },
78
- tags: ['tag1', 'tag2'],
79
- };
80
-
81
- const mockResponse: apiGateway.CreateTraceResponse = {
82
- status: {
83
- code: ResponseStatus_StatusCode.STATUS_CODE_SUCCESS,
84
- errorMessage: undefined
85
- },
86
- traceId: 'trace-123',
87
- };
88
-
89
- mockApiGatewayClient.CreateTrace.mockResolvedValue(mockResponse);
90
-
91
- const result = await parallaxClient.createTrace(mockRequest);
92
-
93
- expect(result).toEqual(mockResponse);
94
- expect(mockApiGatewayClient.CreateTrace).toHaveBeenCalledWith(mockRequest);
95
- expect(mockApiGatewayClient.CreateTrace).toHaveBeenCalledTimes(1);
96
- });
97
-
98
- it('should handle errors when creating a trace', async () => {
99
- const mockRequest: apiGateway.CreateTraceRequest = {
100
- name: 'Test Trace',
101
- attributes: {},
102
- tags: ['tag1', 'tag2'],
103
- };
104
-
105
- const mockError = new Error('gRPC-Web connection failed');
106
- mockApiGatewayClient.CreateTrace.mockRejectedValue(mockError);
107
-
108
- await expect(parallaxClient.createTrace(mockRequest)).rejects.toThrow('gRPC-Web connection failed');
109
- expect(mockConsoleError).toHaveBeenCalledWith(
110
- '[ParallaxClient][createTrace] Error:',
111
- expect.any(Error)
112
- );
131
+ describe('trace()', () => {
132
+ it('should return a ParallaxTrace instance', () => {
133
+ const client = new ParallaxClient('test-key');
134
+ const trace = client.trace('TestTrace');
135
+
136
+ expect(trace).toBeInstanceOf(ParallaxTrace);
137
+ });
138
+
139
+ it('should pass name to ParallaxTrace', () => {
140
+ const client = new ParallaxClient('test-key');
141
+ const trace = client.trace('MyTraceName');
142
+
143
+ // The name is stored internally, we can verify by submitting
144
+ expect(trace).toBeInstanceOf(ParallaxTrace);
145
+ });
146
+
147
+ it('should pass includeClientMeta flag to ParallaxTrace', () => {
148
+ const client = new ParallaxClient('test-key');
149
+ const trace = client.trace('TestTrace', true);
150
+
151
+ expect(trace).toBeInstanceOf(ParallaxTrace);
113
152
  });
114
153
  });
154
+ });
115
155
 
116
- describe('startSpan', () => {
117
- it('should start a span successfully', async () => {
118
- const mockRequest: apiGateway.StartSpanRequest = {
119
- name: 'Test Span',
120
- traceId: 'trace-123',
121
- parentSpanId: undefined,
122
- attributes: {
123
- 'span.type': 'http'
124
- },
125
- startTime: undefined,
126
- };
127
-
128
- const mockResponse: apiGateway.StartSpanResponse = {
129
- status: {
130
- code: ResponseStatus_StatusCode.STATUS_CODE_SUCCESS,
131
- errorMessage: undefined
132
- },
133
- spanId: 'span-456',
134
- };
135
-
136
- mockApiGatewayClient.StartSpan.mockResolvedValue(mockResponse);
137
-
138
- const result = await parallaxClient.startSpan(mockRequest);
139
-
140
- expect(result).toEqual(mockResponse);
141
- expect(mockApiGatewayClient.StartSpan).toHaveBeenCalledWith(mockRequest);
142
- expect(mockApiGatewayClient.StartSpan).toHaveBeenCalledTimes(1);
143
- });
144
-
145
- it('should handle errors when starting a span', async () => {
146
- const mockRequest: apiGateway.StartSpanRequest = {
147
- name: 'Test Span',
148
- traceId: 'trace-123',
149
- attributes: {},
150
- };
151
-
152
- const mockError = new Error('Span creation failed');
153
- mockApiGatewayClient.StartSpan.mockRejectedValue(mockError);
154
-
155
- await expect(parallaxClient.startSpan(mockRequest)).rejects.toThrow('Span creation failed');
156
- expect(mockConsoleError).toHaveBeenCalledWith(
157
- '[ParallaxClient][startSpan] Error:',
158
- expect.any(Error)
159
- );
156
+ describe('ParallaxTrace', () => {
157
+ let client: ParallaxClient;
158
+ let mockCreateTrace: jest.Mock;
159
+
160
+ beforeEach(() => {
161
+ jest.clearAllMocks();
162
+
163
+ mockCreateTrace = jest.fn().mockResolvedValue({
164
+ getTraceId: () => 'trace-456',
165
+ getStatus: () => ({ getCode: () => 1 }),
160
166
  });
167
+
168
+ (ParallaxGatewayServiceClient as jest.Mock).mockImplementation(() => ({
169
+ createTrace: mockCreateTrace,
170
+ }));
171
+
172
+ (global.fetch as jest.Mock).mockResolvedValue({
173
+ ok: true,
174
+ json: () => Promise.resolve({ ip: '192.168.1.1' }),
175
+ });
176
+
177
+ client = new ParallaxClient('test-api-key');
161
178
  });
162
179
 
163
- describe('finishSpan', () => {
164
- it('should finish a span successfully', async () => {
165
- const mockRequest: apiGateway.FinishSpanRequest = {
166
- traceId: 'trace-123',
167
- spanId: 'span-456',
168
- endTime: undefined,
169
- status: undefined,
170
- };
180
+ describe('builder methods', () => {
181
+ it('addAttribute() should return this for chaining', () => {
182
+ const trace = client.trace('TestTrace');
183
+ const result = trace.addAttribute('key', 'value');
171
184
 
172
- const mockResponse: apiGateway.FinishSpanResponse = {
173
- status: {
174
- code: ResponseStatus_StatusCode.STATUS_CODE_SUCCESS,
175
- errorMessage: undefined
176
- },
177
- };
185
+ expect(result).toBe(trace);
186
+ });
187
+
188
+ it('addAttributes() should return this for chaining', () => {
189
+ const trace = client.trace('TestTrace');
190
+ const result = trace.addAttributes({ key1: 'value1', key2: 'value2' });
178
191
 
179
- mockApiGatewayClient.FinishSpan.mockResolvedValue(mockResponse);
192
+ expect(result).toBe(trace);
193
+ });
180
194
 
181
- const result = await parallaxClient.finishSpan(mockRequest);
195
+ it('addTag() should return this for chaining', () => {
196
+ const trace = client.trace('TestTrace');
197
+ const result = trace.addTag('tag1');
182
198
 
183
- expect(result).toEqual(mockResponse);
184
- expect(mockApiGatewayClient.FinishSpan).toHaveBeenCalledWith(mockRequest);
185
- expect(mockApiGatewayClient.FinishSpan).toHaveBeenCalledTimes(1);
199
+ expect(result).toBe(trace);
186
200
  });
187
201
 
188
- it('should handle errors when finishing a span', async () => {
189
- const mockRequest: apiGateway.FinishSpanRequest = {
190
- traceId: 'trace-123',
191
- spanId: 'span-456',
192
- };
202
+ it('addTags() should return this for chaining', () => {
203
+ const trace = client.trace('TestTrace');
204
+ const result = trace.addTags(['tag1', 'tag2']);
193
205
 
194
- const mockError = new Error('Finish span failed');
195
- mockApiGatewayClient.FinishSpan.mockRejectedValue(mockError);
206
+ expect(result).toBe(trace);
207
+ });
196
208
 
197
- await expect(parallaxClient.finishSpan(mockRequest)).rejects.toThrow('Finish span failed');
198
- expect(mockConsoleError).toHaveBeenCalledWith(
199
- '[ParallaxClient][finishSpan] Error:',
200
- expect.any(Error)
201
- );
209
+ it('addEvent() should return this for chaining', () => {
210
+ const trace = client.trace('TestTrace');
211
+ const result = trace.addEvent('event_name');
212
+
213
+ expect(result).toBe(trace);
202
214
  });
203
- });
204
215
 
205
- describe('addSpanAttributes', () => {
206
- it('should add span attributes successfully', async () => {
207
- const mockRequest: apiGateway.AddSpanAttributesRequest = {
208
- traceId: 'trace-123',
209
- spanId: 'span-456',
210
- attributes: {
211
- key1: 'value1',
212
- key2: 'value2',
213
- },
214
- };
215
-
216
- const mockResponse: apiGateway.AddSpanAttributesResponse = {
217
- status: {
218
- code: ResponseStatus_StatusCode.STATUS_CODE_SUCCESS,
219
- errorMessage: undefined
220
- },
221
- };
222
-
223
- mockApiGatewayClient.AddSpanAttributes.mockResolvedValue(mockResponse);
224
-
225
- const result = await parallaxClient.addSpanAttributes(mockRequest);
226
-
227
- expect(result).toEqual(mockResponse);
228
- expect(mockApiGatewayClient.AddSpanAttributes).toHaveBeenCalledWith(mockRequest);
229
- expect(mockApiGatewayClient.AddSpanAttributes).toHaveBeenCalledTimes(1);
230
- });
231
-
232
- it('should handle errors when adding span attributes', async () => {
233
- const mockRequest: apiGateway.AddSpanAttributesRequest = {
234
- traceId: 'trace-123',
235
- spanId: 'span-456',
236
- attributes: {},
237
- };
238
-
239
- const mockError = new Error('Add attributes failed');
240
- mockApiGatewayClient.AddSpanAttributes.mockRejectedValue(mockError);
241
-
242
- await expect(parallaxClient.addSpanAttributes(mockRequest)).rejects.toThrow('Add attributes failed');
243
- expect(mockConsoleError).toHaveBeenCalledWith(
244
- '[ParallaxClient][addSpanAttributes] Error:',
245
- expect.any(Error)
246
- );
216
+ it('addEvent() should accept object details', () => {
217
+ const trace = client.trace('TestTrace');
218
+ const result = trace.addEvent('event_name', { key: 'value' });
219
+
220
+ expect(result).toBe(trace);
221
+ });
222
+
223
+ it('setTxHint() should return this for chaining', () => {
224
+ const trace = client.trace('TestTrace');
225
+ const result = trace.setTxHint('0x123', 'ethereum');
226
+
227
+ expect(result).toBe(trace);
228
+ });
229
+
230
+ it('addAttribute() should accept and stringify objects', () => {
231
+ const trace = client.trace('TestTrace');
232
+ const result = trace.addAttribute('data', { nested: 'value', count: 42 });
233
+
234
+ expect(result).toBe(trace);
235
+ });
236
+
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);
247
245
  });
248
246
  });
249
247
 
250
- describe('addSpanEvent', () => {
251
- it('should add span event successfully', async () => {
252
- const mockRequest: apiGateway.AddSpanEventRequest = {
253
- traceId: 'trace-123',
254
- spanId: 'span-456',
255
- eventName: 'Test Event',
256
- attributes: {
257
- eventType: 'custom',
258
- },
259
- timestamp: undefined,
260
- };
261
-
262
- const mockResponse: apiGateway.AddSpanEventResponse = {
263
- status: {
264
- code: ResponseStatus_StatusCode.STATUS_CODE_SUCCESS,
265
- errorMessage: undefined
266
- },
267
- };
268
-
269
- mockApiGatewayClient.AddSpanEvent.mockResolvedValue(mockResponse);
270
-
271
- const result = await parallaxClient.addSpanEvent(mockRequest);
272
-
273
- expect(result).toEqual(mockResponse);
274
- expect(mockApiGatewayClient.AddSpanEvent).toHaveBeenCalledWith(mockRequest);
275
- expect(mockApiGatewayClient.AddSpanEvent).toHaveBeenCalledTimes(1);
276
- });
277
-
278
- it('should handle errors when adding span event', async () => {
279
- const mockRequest: apiGateway.AddSpanEventRequest = {
280
- traceId: 'trace-123',
281
- spanId: 'span-456',
282
- eventName: 'Test Event',
283
- attributes: {},
284
- };
285
-
286
- const mockError = new Error('Add event failed');
287
- mockApiGatewayClient.AddSpanEvent.mockRejectedValue(mockError);
288
-
289
- await expect(parallaxClient.addSpanEvent(mockRequest)).rejects.toThrow('Add event failed');
290
- expect(mockConsoleError).toHaveBeenCalledWith(
291
- '[ParallaxClient][addSpanEvent] Error:',
292
- expect.any(Error)
293
- );
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('');
294
265
  });
295
266
  });
296
267
 
297
- describe('addSpanError', () => {
298
- it('should add span error successfully', async () => {
299
- const mockRequest: apiGateway.AddSpanErrorRequest = {
300
- traceId: 'trace-123',
301
- spanId: 'span-456',
302
- errorType: 'RuntimeError',
303
- message: 'Something went wrong',
304
- stackTrace: undefined,
305
- attributes: {},
306
- timestamp: undefined,
307
- };
308
-
309
- const mockResponse: apiGateway.AddSpanErrorResponse = {
310
- status: {
311
- code: ResponseStatus_StatusCode.STATUS_CODE_SUCCESS,
312
- errorMessage: undefined
313
- },
314
- };
315
-
316
- mockApiGatewayClient.AddSpanError.mockResolvedValue(mockResponse);
317
-
318
- const result = await parallaxClient.addSpanError(mockRequest);
319
-
320
- expect(result).toEqual(mockResponse);
321
- expect(mockApiGatewayClient.AddSpanError).toHaveBeenCalledWith(mockRequest);
322
- expect(mockApiGatewayClient.AddSpanError).toHaveBeenCalledTimes(1);
323
- });
324
-
325
- it('should handle errors when adding span error', async () => {
326
- const mockRequest: apiGateway.AddSpanErrorRequest = {
327
- traceId: 'trace-123',
328
- spanId: 'span-456',
329
- errorType: 'Error',
330
- message: 'Error message',
331
- attributes: {},
332
- };
333
-
334
- const mockError = new Error('Add error failed');
335
- mockApiGatewayClient.AddSpanError.mockRejectedValue(mockError);
336
-
337
- await expect(parallaxClient.addSpanError(mockRequest)).rejects.toThrow('Add error failed');
338
- expect(mockConsoleError).toHaveBeenCalledWith(
339
- '[ParallaxClient][addSpanError] Error:',
340
- expect.any(Error)
341
- );
268
+ describe('method chaining', () => {
269
+ it('should support fluent API pattern', () => {
270
+ const trace = client.trace('TestTrace')
271
+ .addAttribute('from', '0xabc')
272
+ .addAttribute('to', '0xdef')
273
+ .addAttributes({ value: '100', gas: '21000' })
274
+ .addTag('transaction')
275
+ .addTags(['ethereum', 'send'])
276
+ .addEvent('started')
277
+ .addEvent('completed', { success: true })
278
+ .setTxHint('0x123', 'ethereum');
279
+
280
+ expect(trace).toBeInstanceOf(ParallaxTrace);
342
281
  });
343
282
  });
344
283
 
345
- describe('addSpanHint', () => {
346
- it('should add span hint successfully', async () => {
347
- const mockRequest: apiGateway.AddSpanHintRequest = {
348
- traceId: 'trace-123',
349
- parentSpanId: 'span-456',
350
- timestamp: undefined,
351
- chainTransaction: {
352
- txHash: '0x123abc',
353
- chainId: 1,
354
- },
355
- };
356
-
357
- const mockResponse: apiGateway.AddSpanHintResponse = {
358
- status: {
359
- code: ResponseStatus_StatusCode.STATUS_CODE_SUCCESS,
360
- errorMessage: undefined
361
- },
362
- };
363
-
364
- mockApiGatewayClient.AddSpanHint.mockResolvedValue(mockResponse);
365
-
366
- const result = await parallaxClient.addSpanHint(mockRequest);
367
-
368
- expect(result).toEqual(mockResponse);
369
- expect(mockApiGatewayClient.AddSpanHint).toHaveBeenCalledWith(mockRequest);
370
- expect(mockApiGatewayClient.AddSpanHint).toHaveBeenCalledTimes(1);
371
- });
372
-
373
- it('should handle errors when adding span hint', async () => {
374
- const mockRequest: apiGateway.AddSpanHintRequest = {
375
- traceId: 'trace-123',
376
- parentSpanId: 'span-456',
377
- chainTransaction: undefined,
378
- };
379
-
380
- const mockError = new Error('Add hint failed');
381
- mockApiGatewayClient.AddSpanHint.mockRejectedValue(mockError);
382
-
383
- await expect(parallaxClient.addSpanHint(mockRequest)).rejects.toThrow('Add hint failed');
384
- expect(mockConsoleError).toHaveBeenCalledWith(
385
- '[ParallaxClient][addSpanHint] Error:',
284
+ describe('create()', () => {
285
+ it('should create CreateTraceRequest with attributes', async () => {
286
+ await client.trace('TestTrace')
287
+ .addAttribute('key1', 'value1')
288
+ .addAttribute('key2', 'value2')
289
+ .create();
290
+
291
+ expect(mockCreateTrace).toHaveBeenCalled();
292
+ const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
293
+ expect(request.getName()).toBe('TestTrace');
294
+
295
+ const attrsMap = request.getAttributesMap();
296
+ expect(attrsMap.get('key1')).toBe('value1');
297
+ expect(attrsMap.get('key2')).toBe('value2');
298
+ });
299
+
300
+ it('should stringify object attributes in request', async () => {
301
+ await client.trace('TestTrace', false)
302
+ .addAttribute('config', { timeout: 5000, retries: 3 })
303
+ .addAttributes({ data: { nested: 'value' }, count: 42 })
304
+ .create();
305
+
306
+ const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
307
+ const attrsMap = request.getAttributesMap();
308
+
309
+ expect(attrsMap.get('config')).toBe('{"timeout":5000,"retries":3}');
310
+ expect(attrsMap.get('data')).toBe('{"nested":"value"}');
311
+ expect(attrsMap.get('count')).toBe('42');
312
+ });
313
+
314
+ it('should include tags in request', async () => {
315
+ await client.trace('TestTrace')
316
+ .addTags(['tag1', 'tag2', 'tag3'])
317
+ .create();
318
+
319
+ const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
320
+ expect(request.getTagsList()).toEqual(['tag1', 'tag2', 'tag3']);
321
+ });
322
+
323
+ it('should include events in request', async () => {
324
+ await client.trace('TestTrace')
325
+ .addEvent('event1', 'details1')
326
+ .addEvent('event2', { key: 'value' })
327
+ .create();
328
+
329
+ const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
330
+ const events = request.getEventsList();
331
+ expect(events.length).toBe(2);
332
+ expect(events[0].getName()).toBe('event1');
333
+ expect(events[1].getName()).toBe('event2');
334
+ });
335
+
336
+ it('should include txHashHint when set via setTxHint()', async () => {
337
+ await client.trace('TestTrace')
338
+ .setTxHint('0xabc123', 'ethereum', 'transaction details')
339
+ .create();
340
+
341
+ const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
342
+ const txHint = request.getTxHashHint();
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();
352
+
353
+ const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
354
+ const txHint = request.getTxHashHint();
355
+ expect(txHint?.getChain()).toBe(Chain.CHAIN_POLYGON);
356
+ });
357
+
358
+ it('should include client metadata by default', async () => {
359
+ await client.trace('TestTrace') // includeClientMeta defaults to true
360
+ .addAttribute('custom', 'value')
361
+ .create();
362
+
363
+ const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
364
+ const attrsMap = request.getAttributesMap();
365
+
366
+ // Custom attribute should be present
367
+ expect(attrsMap.get('custom')).toBe('value');
368
+
369
+ // Client metadata should be prefixed with 'client.'
370
+ expect(attrsMap.get('client.browser')).toBe('Chrome');
371
+ expect(attrsMap.get('client.os')).toBe('macOS');
372
+ });
373
+
374
+ it('should not include client metadata when explicitly disabled', async () => {
375
+ await client.trace('TestTrace', false)
376
+ .addAttribute('custom', 'value')
377
+ .create();
378
+
379
+ const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
380
+ const attrsMap = request.getAttributesMap();
381
+
382
+ expect(attrsMap.get('custom')).toBe('value');
383
+ expect(attrsMap.get('client.browser')).toBeUndefined();
384
+ });
385
+
386
+ it('should return traceId from create()', async () => {
387
+ const traceId = await client.trace('TestTrace').create();
388
+
389
+ expect(traceId).toBe('trace-456');
390
+ });
391
+
392
+ it('should return undefined and log on connection error', async () => {
393
+ mockCreateTrace.mockRejectedValue(new Error('Connection failed'));
394
+
395
+ const traceId = await client.trace('TestTrace').create();
396
+
397
+ expect(traceId).toBeUndefined();
398
+ expect(mockConsoleLog).toHaveBeenCalledWith(
399
+ '[ParallaxTrace] Error creating trace:',
386
400
  expect.any(Error)
387
401
  );
388
402
  });
389
- });
390
403
 
391
- describe('integration scenarios', () => {
392
- it('should handle multiple method calls in sequence', async () => {
393
- const traceRequest: apiGateway.CreateTraceRequest = {
394
- name: 'Integration Test',
395
- attributes: {
396
- 'project.id': 'test-project'
397
- },
398
- tags: ['tag1', 'tag2'],
399
- };
400
-
401
- const spanRequest: apiGateway.StartSpanRequest = {
402
- name: 'Integration Span',
403
- traceId: 'trace-123',
404
- attributes: {},
405
- };
406
-
407
- mockApiGatewayClient.CreateTrace.mockResolvedValue({
408
- traceId: 'trace-123',
409
- status: {
410
- code: ResponseStatus_StatusCode.STATUS_CODE_SUCCESS,
411
- errorMessage: undefined
412
- }
413
- });
414
- mockApiGatewayClient.StartSpan.mockResolvedValue({
415
- spanId: 'span-456',
416
- status: {
417
- code: ResponseStatus_StatusCode.STATUS_CODE_SUCCESS,
418
- errorMessage: undefined
419
- }
404
+ it('should return undefined and log on status error', async () => {
405
+ mockCreateTrace.mockResolvedValue({
406
+ getTraceId: () => '',
407
+ getStatus: () => ({
408
+ getCode: () => 4, // INTERNAL_ERROR
409
+ getErrorMessage: () => 'Internal server error',
410
+ }),
420
411
  });
421
412
 
422
- await parallaxClient.createTrace(traceRequest);
423
- await parallaxClient.startSpan(spanRequest);
413
+ const traceId = await client.trace('TestTrace').create();
424
414
 
425
- expect(mockApiGatewayClient.CreateTrace).toHaveBeenCalledTimes(1);
426
- expect(mockApiGatewayClient.StartSpan).toHaveBeenCalledTimes(1);
415
+ expect(traceId).toBeUndefined();
416
+ expect(mockConsoleLog).toHaveBeenCalledWith(
417
+ '[ParallaxTrace] Error:',
418
+ 'Internal server error'
419
+ );
427
420
  });
428
421
 
429
- it('should create client instances with different API keys', () => {
430
- const client1 = new ParallaxClient('key1');
431
- const client2 = new ParallaxClient('key2');
432
- const client3 = new ParallaxClient();
422
+ it('should work without txHashHint', async () => {
423
+ await client.trace('TestTrace')
424
+ .addAttribute('key', 'value')
425
+ .create();
426
+
427
+ const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
428
+ expect(request.getTxHashHint()).toBeUndefined();
429
+ });
430
+ });
433
431
 
434
- expect(client1.apiKey).toBe('key1');
435
- expect(client2.apiKey).toBe('key2');
436
- expect(client3.apiKey).toBeUndefined();
432
+ describe('integration test', () => {
433
+ it('should work with real usage pattern', async () => {
434
+ const traceId = await client.trace('SendTransaction') // client metadata included by default
435
+ .addAttribute('from', '0xabc123')
436
+ .addAttribute('to', '0xdef456')
437
+ .addAttribute('value', '1.5')
438
+ .addAttribute('network', 'ethereum')
439
+ .addTags(['transaction', 'send', 'ethereum'])
440
+ .addEvent('wallet_connected', { wallet: 'MetaMask' })
441
+ .addEvent('transaction_initiated')
442
+ .addEvent('transaction_sent', { blockNumber: 12345 })
443
+ .setTxHint('0xtxhash123', 'ethereum')
444
+ .create();
445
+
446
+ expect(traceId).toBe('trace-456');
447
+ expect(mockCreateTrace).toHaveBeenCalledTimes(1);
448
+
449
+ const request = mockCreateTrace.mock.calls[0][0] as CreateTraceRequest;
450
+ expect(request.getName()).toBe('SendTransaction');
451
+ expect(request.getTagsList()).toEqual(['transaction', 'send', 'ethereum']);
452
+ expect(request.getEventsList().length).toBe(3);
453
+ expect(request.getTxHashHint()?.getTxHash()).toBe('0xtxhash123');
454
+ expect(request.getTxHashHint()?.getChain()).toBe(Chain.CHAIN_ETHEREUM);
455
+
456
+ const attrsMap = request.getAttributesMap();
457
+ expect(attrsMap.get('from')).toBe('0xabc123');
458
+ expect(attrsMap.get('to')).toBe('0xdef456');
459
+ expect(attrsMap.get('client.browser')).toBe('Chrome');
437
460
  });
438
461
  });
439
462
  });