@@ -2,49 +2,251 @@ var pluginOb = {},
22 parser = require ( 'ua-parser-js' ) ,
33 plugins = require ( '../../pluginManager.js' ) ;
44
5+ /**
6+ * Normalize client hints OS version to user-facing values where needed
7+ * @param {string } os - OS name
8+ * @param {string } version - OS version from client hints
9+ * @returns {string|null } normalized OS version
10+ */
11+ function normalizeClientHintsOsVersion ( os , version ) {
12+ if ( ! version ) {
13+ return version ;
14+ }
15+
16+ if ( os === 'Windows' ) {
17+ var versionParts = ( version + '' ) . split ( '.' ) ;
18+ var major = parseInt ( versionParts [ 0 ] , 10 ) ;
19+ var minor = parseInt ( versionParts [ 1 ] , 10 ) ;
20+ if ( ! isNaN ( major ) ) {
21+ if ( major >= 13 ) {
22+ return '11' ;
23+ }
24+ if ( major >= 10 ) {
25+ return '10' ;
26+ }
27+ if ( major === 6 ) {
28+ if ( minor >= 3 ) {
29+ return '8.1' ;
30+ }
31+ if ( minor === 2 ) {
32+ return '8' ;
33+ }
34+ if ( minor === 1 ) {
35+ return '7' ;
36+ }
37+ if ( minor === 0 ) {
38+ return 'Vista' ;
39+ }
40+ }
41+ if ( major === 5 ) {
42+ if ( minor >= 1 ) {
43+ return 'XP' ;
44+ }
45+ return '2000' ;
46+ }
47+ }
48+ }
49+
50+ return version ;
51+ }
52+
53+ /**
54+ * Parse client hints headers to extract browser, OS, and device information
55+ * @param {object } headers - HTTP request headers
56+ * @returns {object } Parsed client hints data
57+ */
58+ function parseClientHints ( headers ) {
59+ var hints = {
60+ browser : null ,
61+ browserVersion : null ,
62+ os : null ,
63+ osVersion : null ,
64+ mobile : null ,
65+ device : null
66+ } ;
67+
68+ // Parse Sec-CH-UA header for browser info
69+ // Format: "Chromium";v="110", "Google Chrome";v="110", "Not=A?Brand";v="99"
70+ var secChUa = headers [ 'sec-ch-ua' ] ;
71+ if ( secChUa ) {
72+ var brands = secChUa . split ( ',' ) . map ( function ( brand ) {
73+ var match = brand . trim ( ) . match ( / " ( [ ^ " ] + ) " ; v = " ( [ ^ " ] + ) " / ) ;
74+ if ( match ) {
75+ return { name : match [ 1 ] , version : match [ 2 ] } ;
76+ }
77+ return null ;
78+ } ) . filter ( Boolean ) ;
79+
80+ // Filter out placeholder brands
81+ var validBrands = brands . filter ( function ( brand ) {
82+ return ! brand . name . includes ( 'Not' ) && ! brand . name . includes ( '?' ) ;
83+ } ) ;
84+
85+ if ( validBrands . length > 0 ) {
86+ // Prefer specific browser over Chromium
87+ var preferredBrand = validBrands . find ( function ( b ) {
88+ return b . name !== 'Chromium' ;
89+ } ) || validBrands [ 0 ] ;
90+
91+ hints . browser = preferredBrand . name ;
92+ hints . browserVersion = preferredBrand . version ;
93+ }
94+ }
95+
96+ // Parse full version if available.
97+ // `sec-ch-ua-full-version` is a single quoted version string (e.g. "120.0.6099.230").
98+ var secChUaFullVersion = headers [ 'sec-ch-ua-full-version' ] ;
99+ if ( secChUaFullVersion && secChUaFullVersion . startsWith ( '"' ) ) {
100+ var fullVersionMatch = secChUaFullVersion . match ( / " ( [ ^ " ] + ) " / ) ;
101+ if ( fullVersionMatch ) {
102+ hints . browserVersion = fullVersionMatch [ 1 ] ;
103+ }
104+ }
105+
106+ // `sec-ch-ua-full-version-list` is a brand list (e.g. "Chromium";v="120", "Google Chrome";v="120").
107+ // Parse it like sec-ch-ua and pick the version for the preferred browser brand.
108+ var secChUaFullVersionList = headers [ 'sec-ch-ua-full-version-list' ] ;
109+ if ( secChUaFullVersionList ) {
110+ var fullVersionBrands = secChUaFullVersionList . split ( ',' ) . map ( function ( brand ) {
111+ var match = brand . trim ( ) . match ( / " ( [ ^ " ] + ) " ; v = " ( [ ^ " ] + ) " / ) ;
112+ if ( match ) {
113+ return { name : match [ 1 ] , version : match [ 2 ] } ;
114+ }
115+ return null ;
116+ } ) . filter ( Boolean ) . filter ( function ( brand ) {
117+ return ! brand . name . includes ( 'Not' ) && ! brand . name . includes ( '?' ) ;
118+ } ) ;
119+
120+ if ( fullVersionBrands . length > 0 ) {
121+ var preferredFullVersionBrand = null ;
122+
123+ if ( hints . browser ) {
124+ preferredFullVersionBrand = fullVersionBrands . find ( function ( b ) {
125+ return b . name === hints . browser ;
126+ } ) ;
127+ }
128+
129+ if ( ! preferredFullVersionBrand ) {
130+ preferredFullVersionBrand = fullVersionBrands . find ( function ( b ) {
131+ return b . name !== 'Chromium' ;
132+ } ) || fullVersionBrands [ 0 ] ;
133+ }
134+
135+ hints . browserVersion = preferredFullVersionBrand . version ;
136+ }
137+ }
138+
139+ // Parse platform (OS)
140+ var secChUaPlatform = headers [ 'sec-ch-ua-platform' ] ;
141+ if ( secChUaPlatform ) {
142+ hints . os = secChUaPlatform . replace ( / " / g, '' ) ;
143+
144+ // Normalize platform names to match ua-parser-js format
145+ if ( hints . os === 'macOS' ) {
146+ hints . os = 'Mac OS' ;
147+ }
148+ else if ( hints . os === 'Windows' ) {
149+ hints . os = 'Windows' ;
150+ }
151+ else if ( hints . os === 'Linux' ) {
152+ hints . os = 'Linux' ;
153+ }
154+ else if ( hints . os === 'Chrome OS' ) {
155+ hints . os = 'Chrome OS' ;
156+ }
157+ }
158+
159+ // Parse platform version
160+ var secChUaPlatformVersion = headers [ 'sec-ch-ua-platform-version' ] ;
161+ if ( secChUaPlatformVersion ) {
162+ hints . osVersion = normalizeClientHintsOsVersion ( hints . os , secChUaPlatformVersion . replace ( / " / g, '' ) ) ;
163+ }
164+
165+ // Parse mobile indicator
166+ var secChUaMobile = headers [ 'sec-ch-ua-mobile' ] ;
167+ if ( secChUaMobile ) {
168+ hints . mobile = secChUaMobile === '?1' ;
169+ }
170+
171+ // Parse device model
172+ var secChUaModel = headers [ 'sec-ch-ua-model' ] ;
173+ if ( secChUaModel ) {
174+ hints . device = secChUaModel . replace ( / " / g, '' ) ;
175+ }
176+
177+ return hints ;
178+ }
179+
5180( function ( ) {
6181 plugins . appTypes . push ( "web" ) ;
7182
8183 plugins . register ( "/sdk/pre" , function ( ob ) {
9184 var params = ob . params ;
10185
186+ // Parse client hints first (modern approach)
187+ var clientHints = parseClientHints ( params . req . headers ) ;
188+
189+ // Parse user agent as fallback
11190 var agent = parser ( ( params . qstring . metrics && params . qstring . metrics . _ua ) ? params . qstring . metrics . _ua : params . req . headers [ 'user-agent' ] ) ;
12- var data = { os : agent . os . name , os_version : agent . os . version } ;
13191
192+ // Merge client hints with user agent data (client hints take priority)
193+ var data = {
194+ os : clientHints . os || agent . os . name ,
195+ os_version : clientHints . osVersion || agent . os . version ,
196+ browser : clientHints . browser || agent . browser . name ,
197+ browser_version : clientHints . browserVersion || agent . browser . version ,
198+ mobile : clientHints . mobile ,
199+ device : clientHints . device
200+ } ;
201+
202+ // Normalize OS name
14203 if ( data . os === "Mac OS" ) {
15204 data . os = "Mac" ;
16205 }
17- else if ( data . os === "iOS" || data . os === "Android" ) {
18- if ( agent . browser . name === "Firefox" ) {
19- agent . browser . name = "Firefox Mobile" ;
206+
207+ // Detect mobile browsers based on OS and mobile flag
208+ var isMobile = data . mobile !== null ? data . mobile : ( data . os === "iOS" || data . os === "Android" ) ;
209+
210+ if ( isMobile || data . os === "iOS" || data . os === "Android" ) {
211+ if ( data . browser === "Firefox" ) {
212+ data . browser = "Firefox Mobile" ;
20213 }
21- else if ( agent . browser . name === "Chrome" ) {
22- agent . browser . name = "Chrome Mobile" ;
214+ else if ( data . browser === "Chrome" || data . browser === "Google Chrome" ) {
215+ data . browser = "Chrome Mobile" ;
23216 }
24- else if ( agent . browser . name === "Edge" ) {
25- agent . browser . name = "Edge Mobile" ;
217+ else if ( data . browser === "Edge" || data . browser === "Microsoft Edge" ) {
218+ data . browser = "Edge Mobile" ;
26219 }
27220 }
28221
29- if ( agent . browser . name === "Edge" ) {
30- if ( agent . engine . name === "WebKit" ) {
31- agent . browser . name = "Edge Chromium" ;
222+ // Detect Edge Chromium
223+ if ( data . browser === "Edge" || data . browser === "Microsoft Edge" || agent . browser . name === "Edge" ) {
224+ if ( agent . engine . name === "WebKit" || agent . engine . name === "Blink" ) {
225+ data . browser = "Edge Chromium" ;
32226 }
33227 }
34228
229+ // Normalize browser names from client hints
230+ if ( data . browser === "Google Chrome" ) {
231+ data . browser = "Chrome" ;
232+ }
233+ else if ( data . browser === "Microsoft Edge" ) {
234+ data . browser = "Edge" ;
235+ }
236+
35237 if ( params . qstring . begin_session ) {
36- //try to add metrics based on user agent
238+ //try to add metrics based on user agent and client hints
37239 if ( ! params . qstring . metrics ) {
38240 params . qstring . metrics = { } ;
39241 }
40242
41- //if some metrics are not provided, parse them from user agent
243+ //if some metrics are not provided, parse them from client hints or user agent
42244 if ( ! params . qstring . metrics . _browser ) {
43- params . qstring . metrics . _browser = agent . browser . name ;
245+ params . qstring . metrics . _browser = data . browser ;
44246 }
45247
46248 if ( ! params . qstring . metrics . _browser_version ) {
47- params . qstring . metrics . _browser_version = agent . browser . version ;
249+ params . qstring . metrics . _browser_version = data . browser_version ;
48250 }
49251
50252 if ( params . qstring . metrics . _browser && params . qstring . metrics . _browser_version && ! params . qstring . metrics . _browser_version . startsWith ( "[" + params . qstring . metrics . _browser . toLowerCase ( ) + "]_" ) ) {
@@ -60,7 +262,11 @@ var pluginOb = {},
60262 }
61263
62264 if ( ! params . qstring . metrics . _device ) {
63- if ( typeof agent . device . model !== "undefined" ) {
265+ // Prioritize client hints device model
266+ if ( data . device ) {
267+ params . qstring . metrics . _device = data . device ;
268+ }
269+ else if ( typeof agent . device . model !== "undefined" ) {
64270 params . qstring . metrics . _device = agent . device . model ;
65271 }
66272 else {
@@ -69,7 +275,13 @@ var pluginOb = {},
69275 }
70276
71277 if ( ! params . qstring . metrics . _device_type ) {
72- params . qstring . metrics . _device_type = agent . device . type ;
278+ // Determine device type from client hints mobile flag or user agent
279+ if ( data . mobile === true ) {
280+ params . qstring . metrics . _device_type = "mobile" ;
281+ }
282+ else {
283+ params . qstring . metrics . _device_type = agent . device . type ;
284+ }
73285
74286 //if still undefined and app is web then it must be desktop
75287 if ( ! params . qstring . metrics . _device_type && params . app . type === "web" ) {
@@ -118,11 +330,15 @@ var pluginOb = {},
118330 }
119331
120332 if ( ! params . qstring . crash . _browser ) {
121- params . qstring . crash . _browser = agent . browser . name ;
333+ params . qstring . crash . _browser = data . browser ;
122334 }
123335
124336 if ( ! params . qstring . crash . _device ) {
125- if ( typeof agent . device . model !== "undefined" ) {
337+ // Prioritize client hints device model
338+ if ( data . device ) {
339+ params . qstring . crash . _device = data . device ;
340+ }
341+ else if ( typeof agent . device . model !== "undefined" ) {
126342 params . qstring . crash . _device = agent . device . model ;
127343 }
128344 else {
0 commit comments