From 390a0259fcfb9fbb537c685a19b99fbd65ba699b Mon Sep 17 00:00:00 2001 From: Fabricio Campos Date: Wed, 28 Jan 2026 15:46:34 -0300 Subject: [PATCH] prevents trim is not a function exception --- packages/node/src/exposure/exposure.ts | 13 ++++++++-- .../local/exposure/exposure-filter.test.ts | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/node/src/exposure/exposure.ts b/packages/node/src/exposure/exposure.ts index 8293d6a..93f766b 100644 --- a/packages/node/src/exposure/exposure.ts +++ b/packages/node/src/exposure/exposure.ts @@ -29,11 +29,20 @@ export class Exposure { } public canonicalize(): string { - let canonical = `${this.user.user_id?.trim()} ${this.user.device_id?.trim()} `; + const userId = + this.user.user_id != null + ? String(this.user.user_id).trim() + : this.user.user_id; + const deviceId = + this.user.device_id != null + ? String(this.user.device_id).trim() + : this.user.device_id; + let canonical = `${userId} ${deviceId} `; for (const key of Object.keys(this.results).sort()) { const variant = this.results[key]; if (variant?.key) { - canonical += key.trim() + ' ' + variant?.key?.trim() + ' '; + const variantKey = String(variant.key).trim(); + canonical += key.trim() + ' ' + variantKey + ' '; } } return canonical; diff --git a/packages/node/test/local/exposure/exposure-filter.test.ts b/packages/node/test/local/exposure/exposure-filter.test.ts index 9cff948..ee32730 100644 --- a/packages/node/test/local/exposure/exposure-filter.test.ts +++ b/packages/node/test/local/exposure/exposure-filter.test.ts @@ -130,3 +130,29 @@ test('filter - ttl-based eviction', async () => { await sleep(950); expect(filter.shouldTrack(exposure2)).toEqual(false); }); + +test('filter - non-string user_id should not throw', async () => { + // Simulate runtime scenario where user_id is a number (e.g., from untyped JS or JSON) + const user = { user_id: 12345 } as unknown as ExperimentUser; + const results = { + 'flag-key-1': { key: 'on', value: 'on' }, + }; + const filter = new InMemoryExposureFilter(100); + const exposure = new Exposure(user, results); + // Should not throw TypeError: trim is not a function + expect(() => filter.shouldTrack(exposure)).not.toThrow(); + expect(filter.shouldTrack(exposure)).toEqual(false); // Already tracked +}); + +test('filter - non-string device_id should not throw', async () => { + // Simulate runtime scenario where device_id is a number + const user = { device_id: 67890 } as unknown as ExperimentUser; + const results = { + 'flag-key-1': { key: 'on', value: 'on' }, + }; + const filter = new InMemoryExposureFilter(100); + const exposure = new Exposure(user, results); + // Should not throw TypeError: trim is not a function + expect(() => filter.shouldTrack(exposure)).not.toThrow(); + expect(filter.shouldTrack(exposure)).toEqual(false); // Already tracked +});