From 6e5e63157d5aa0f5d69c2afd0ac030edfce12599 Mon Sep 17 00:00:00 2001 From: avionicharshit-byte Date: Sun, 30 Aug 2026 00:50:17 +0530 Subject: [PATCH] fix(common): don't drop device overrides when the user agent looks like a server --- .../common/server/parser-user-agent.test.ts | 37 +++++++++++++++++++ packages/common/server/parser-user-agent.ts | 14 ++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/common/server/parser-user-agent.test.ts b/packages/common/server/parser-user-agent.test.ts index 917f8566b..888fcd9f0 100644 --- a/packages/common/server/parser-user-agent.test.ts +++ b/packages/common/server/parser-user-agent.test.ts @@ -128,6 +128,43 @@ describe('parseUserAgent', () => { model: 'Custom Model', }); }); + + it('should apply overrides on a non-browser sdk user agent', () => { + // A native SDK sends a plain name/version UA, which the server heuristic + // matches, but it also states what the device is. The overrides win. + const ua = 'my-sdk/1.0.0'; + const overrides = { + __os: 'Android', + __osVersion: '13', + __device: 'mobile', + __brand: 'Acme', + __model: 'A105', + }; + + expect(parseUserAgent(ua, overrides)).toEqual({ + isServer: false, + device: 'mobile', + os: 'Android', + osVersion: '13', + browser: undefined, + browserVersion: undefined, + brand: 'Acme', + model: 'A105', + }); + }); + + it('should still be a server when a non-browser user agent sends no device overrides', () => { + expect(parseUserAgent('my-sdk/1.0.0', { __ip: '1.2.3.4' })).toEqual({ + isServer: true, + device: 'server', + os: '', + osVersion: '', + browser: '', + browserVersion: '', + brand: '', + model: '', + }); + }); }); describe('getDevice', () => { diff --git a/packages/common/server/parser-user-agent.ts b/packages/common/server/parser-user-agent.ts index c859bccb2..783ba5c4a 100644 --- a/packages/common/server/parser-user-agent.ts +++ b/packages/common/server/parser-user-agent.ts @@ -247,7 +247,10 @@ export function parseUserAgent( if (!ua) return parsedServerUa; const res = parse(ua); - if (isServer(res)) { + // A native SDK sends a plain name/version UA, which the server heuristic + // matches, but it also states what device it is running on. Trust that over + // the heuristic, otherwise the overrides are silently dropped. + if (isServer(res) && !hasDeviceOverrides(overrides)) { return parsedServerUa; } @@ -287,6 +290,15 @@ export function parseUserAgent( } as const; } +const DEVICE_OVERRIDE_KEYS = ['__os', '__device', '__brand', '__model'] as const; + +function hasDeviceOverrides(overrides?: Record) { + if (!overrides) return false; + return DEVICE_OVERRIDE_KEYS.some( + (key) => typeof overrides[key] === 'string' && overrides[key] !== '', + ); +} + function isServer(res: UAParser.IResult) { // Matches user agents like "Go-http-client/1.0" or "Go Http Client/1.0" // It should just match the first name (with optional spaces) and version