@posthog/core 1.29.15 → 1.30.1

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,9 +1,23 @@
1
+ /**
2
+ * Hints from sources outside the User-Agent string. These let us identify Brave
3
+ * on desktop / Android — Chromium-based with no UA marker, but it exposes
4
+ * `navigator.brave`. (Brave on iOS is detected via its `Brave/X` UA marker —
5
+ * WebKit doesn't expose `navigator.brave` — so no hint is needed there.)
6
+ */
7
+ export interface BrowserDetectionHints {
8
+ brave?: boolean;
9
+ }
1
10
  /**
2
11
  * This function detects which browser is running this script.
3
12
  * The order of the checks are important since many user agents
4
13
  * include keywords used in later checks.
14
+ *
15
+ * `hints` is an optional bag of out-of-band signals (`navigator.brave`) used to
16
+ * detect browsers that intentionally do not identify themselves in the UA
17
+ * string. When omitted, only UA-string detection runs — preserving the previous
18
+ * behaviour.
5
19
  */
6
- export declare const detectBrowser: (user_agent: string, vendor: string | undefined) => string;
20
+ export declare const detectBrowser: (user_agent: string, vendor: string | undefined, hints?: BrowserDetectionHints) => string;
7
21
  /**
8
22
  * This function detects which browser version is running this script,
9
23
  * parsing major and minor version (e.g., 42.1). User agent strings from:
@@ -12,7 +26,7 @@ export declare const detectBrowser: (user_agent: string, vendor: string | undefi
12
26
  * `navigator.vendor` is passed in and used to help with detecting certain browsers
13
27
  * NB `navigator.vendor` is deprecated and not present in every browser
14
28
  */
15
- export declare const detectBrowserVersion: (userAgent: string, vendor: string | undefined) => number | null;
29
+ export declare const detectBrowserVersion: (userAgent: string, vendor: string | undefined, hints?: BrowserDetectionHints) => number | null;
16
30
  export declare const detectOS: (user_agent: string) => [string, string];
17
31
  export declare const detectDevice: (user_agent: string) => string;
18
32
  export declare const detectDeviceType: (user_agent: string, options?: {
@@ -1 +1 @@
1
- {"version":3,"file":"user-agent-utils.d.ts","sourceRoot":"","sources":["../../src/utils/user-agent-utils.ts"],"names":[],"mappings":"AAsFA;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAa,YAAY,MAAM,EAAE,QAAQ,MAAM,GAAG,SAAS,KAAG,MAmDvF,CAAA;AAuBD;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAAa,WAAW,MAAM,EAAE,QAAQ,MAAM,GAAG,SAAS,KAAG,MAAM,GAAG,IAetG,CAAA;AA0FD,eAAO,MAAM,QAAQ,GAAa,YAAY,MAAM,KAAG,CAAC,MAAM,EAAE,MAAM,CAUrE,CAAA;AAED,eAAO,MAAM,YAAY,GAAa,YAAY,MAAM,KAAG,MAuD1D,CAAA;AAED,eAAO,MAAM,gBAAgB,GAC3B,YAAY,MAAM,EAClB,UAAU;IACR,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,KACA,MA4BF,CAAA"}
1
+ {"version":3,"file":"user-agent-utils.d.ts","sourceRoot":"","sources":["../../src/utils/user-agent-utils.ts"],"names":[],"mappings":"AA+DA;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IAGpC,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAuCD;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GACxB,YAAY,MAAM,EAClB,QAAQ,MAAM,GAAG,SAAS,EAC1B,QAAQ,qBAAqB,KAC5B,MAmFF,CAAA;AAkCD;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,GAC/B,WAAW,MAAM,EACjB,QAAQ,MAAM,GAAG,SAAS,EAC1B,QAAQ,qBAAqB,KAC5B,MAAM,GAAG,IAmBX,CAAA;AA0FD,eAAO,MAAM,QAAQ,GAAa,YAAY,MAAM,KAAG,CAAC,MAAM,EAAE,MAAM,CAUrE,CAAA;AAED,eAAO,MAAM,YAAY,GAAa,YAAY,MAAM,KAAG,MAuD1D,CAAA;AAED,eAAO,MAAM,gBAAgB,GAC3B,YAAY,MAAM,EAClB,UAAU;IACR,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,KACA,MA4BF,CAAA"}
@@ -71,8 +71,19 @@ const GENERIC_MOBILE = GENERIC + ' ' + MOBILE.toLowerCase();
71
71
  const GENERIC_TABLET = GENERIC + ' ' + TABLET.toLowerCase();
72
72
  const KONQUEROR = 'Konqueror';
73
73
  const OCULUS_BROWSER = 'Oculus Browser';
74
+ const VIVALDI = 'Vivaldi';
75
+ const YANDEX = 'Yandex';
76
+ const WHALE = 'Whale';
77
+ const DUCKDUCKGO = 'DuckDuckGo';
78
+ const PALE_MOON = 'Pale Moon';
79
+ const WATERFOX = 'Waterfox';
80
+ const BRAVE = 'Brave';
74
81
  const BROWSER_VERSION_REGEX_SUFFIX = '(\\d+(\\.\\d+)?)';
75
82
  const DEFAULT_BROWSER_VERSION_REGEX = new RegExp('Version/' + BROWSER_VERSION_REGEX_SUFFIX);
83
+ function browserFromHints(hints) {
84
+ if (hints?.brave) return BRAVE;
85
+ return null;
86
+ }
76
87
  const XBOX_REGEX = new RegExp(XBOX, 'i');
77
88
  const PLAYSTATION_REGEX = new RegExp(PLAYSTATION + ' \\w+', 'i');
78
89
  const NINTENDO_REGEX = new RegExp(NINTENDO + ' \\w+', 'i');
@@ -94,8 +105,10 @@ function isSafari(userAgent) {
94
105
  return (0, external_string_utils_js_namespaceObject.includes)(userAgent, SAFARI) && !(0, external_string_utils_js_namespaceObject.includes)(userAgent, CHROME) && !(0, external_string_utils_js_namespaceObject.includes)(userAgent, ANDROID);
95
106
  }
96
107
  const safariCheck = (ua, vendor)=>vendor && (0, external_string_utils_js_namespaceObject.includes)(vendor, APPLE) || isSafari(ua);
97
- const detectBrowser = function(user_agent, vendor) {
108
+ const detectBrowser = function(user_agent, vendor, hints) {
98
109
  vendor = vendor || '';
110
+ const fromHints = browserFromHints(hints);
111
+ if (fromHints) return fromHints;
99
112
  if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, ' OPR/') && (0, external_string_utils_js_namespaceObject.includes)(user_agent, 'Mini')) return OPERA_MINI;
100
113
  if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, ' OPR/')) return OPERA;
101
114
  if (BLACKBERRY_REGEX.test(user_agent)) return BLACKBERRY;
@@ -103,6 +116,10 @@ const detectBrowser = function(user_agent, vendor) {
103
116
  if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'OculusBrowser')) return OCULUS_BROWSER;
104
117
  else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, SAMSUNG_BROWSER)) return SAMSUNG_INTERNET;
105
118
  else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, EDGE) || (0, external_string_utils_js_namespaceObject.includes)(user_agent, 'Edg/')) return MICROSOFT_EDGE;
119
+ else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, VIVALDI + '/')) return VIVALDI;
120
+ else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'YaBrowser/')) return YANDEX;
121
+ else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, WHALE + '/')) return WHALE;
122
+ else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, DUCKDUCKGO + '/') || (0, external_string_utils_js_namespaceObject.includes)(user_agent, 'Ddg/')) return DUCKDUCKGO;
106
123
  else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'FBIOS')) return FACEBOOK + ' ' + MOBILE;
107
124
  else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'UCWEB') || (0, external_string_utils_js_namespaceObject.includes)(user_agent, 'UCBrowser')) return 'UC Browser';
108
125
  else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'CriOS')) return CHROME_IOS;
@@ -111,7 +128,10 @@ const detectBrowser = function(user_agent, vendor) {
111
128
  else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, ANDROID) && (0, external_string_utils_js_namespaceObject.includes)(user_agent, SAFARI)) return ANDROID_MOBILE;
112
129
  else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'FxiOS')) return FIREFOX_IOS;
113
130
  else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent.toLowerCase(), KONQUEROR.toLowerCase())) return KONQUEROR;
131
+ else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, BRAVE + '/')) return BRAVE;
114
132
  else if (safariCheck(user_agent, vendor)) return (0, external_string_utils_js_namespaceObject.includes)(user_agent, MOBILE) ? MOBILE_SAFARI : SAFARI;
133
+ else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'PaleMoon/')) return PALE_MOON;
134
+ else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, WATERFOX + '/')) return WATERFOX;
115
135
  else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, FIREFOX)) return FIREFOX;
116
136
  else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'MSIE') || (0, external_string_utils_js_namespaceObject.includes)(user_agent, 'Trident/')) return INTERNET_EXPLORER;
117
137
  else if ((0, external_string_utils_js_namespaceObject.includes)(user_agent, 'Gecko')) return FIREFOX;
@@ -164,6 +184,27 @@ const versionRegexes = {
164
184
  [OCULUS_BROWSER]: [
165
185
  new RegExp('OculusBrowser\\/' + BROWSER_VERSION_REGEX_SUFFIX)
166
186
  ],
187
+ [VIVALDI]: [
188
+ new RegExp(VIVALDI + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)
189
+ ],
190
+ [YANDEX]: [
191
+ new RegExp('YaBrowser\\/' + BROWSER_VERSION_REGEX_SUFFIX)
192
+ ],
193
+ [WHALE]: [
194
+ new RegExp(WHALE + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)
195
+ ],
196
+ [BRAVE]: [
197
+ new RegExp(BRAVE + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)
198
+ ],
199
+ [DUCKDUCKGO]: [
200
+ new RegExp('(DuckDuckGo|Ddg)\\/' + BROWSER_VERSION_REGEX_SUFFIX)
201
+ ],
202
+ [PALE_MOON]: [
203
+ new RegExp('PaleMoon\\/' + BROWSER_VERSION_REGEX_SUFFIX)
204
+ ],
205
+ [WATERFOX]: [
206
+ new RegExp(WATERFOX + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)
207
+ ],
167
208
  [INTERNET_EXPLORER]: [
168
209
  new RegExp('(rv:|MSIE )' + BROWSER_VERSION_REGEX_SUFFIX)
169
210
  ],
@@ -171,8 +212,8 @@ const versionRegexes = {
171
212
  new RegExp('rv:' + BROWSER_VERSION_REGEX_SUFFIX)
172
213
  ]
173
214
  };
174
- const detectBrowserVersion = function(userAgent, vendor) {
175
- const browser = detectBrowser(userAgent, vendor);
215
+ const detectBrowserVersion = function(userAgent, vendor, hints) {
216
+ const browser = detectBrowser(userAgent, vendor, hints);
176
217
  const regexes = versionRegexes[browser];
177
218
  if ((0, external_type_utils_js_namespaceObject.isUndefined)(regexes)) return null;
178
219
  for(let i = 0; i < regexes.length; i++){
@@ -39,8 +39,19 @@ const GENERIC_MOBILE = GENERIC + ' ' + MOBILE.toLowerCase();
39
39
  const GENERIC_TABLET = GENERIC + ' ' + TABLET.toLowerCase();
40
40
  const KONQUEROR = 'Konqueror';
41
41
  const OCULUS_BROWSER = 'Oculus Browser';
42
+ const VIVALDI = 'Vivaldi';
43
+ const YANDEX = 'Yandex';
44
+ const WHALE = 'Whale';
45
+ const DUCKDUCKGO = 'DuckDuckGo';
46
+ const PALE_MOON = 'Pale Moon';
47
+ const WATERFOX = 'Waterfox';
48
+ const BRAVE = 'Brave';
42
49
  const BROWSER_VERSION_REGEX_SUFFIX = '(\\d+(\\.\\d+)?)';
43
50
  const DEFAULT_BROWSER_VERSION_REGEX = new RegExp('Version/' + BROWSER_VERSION_REGEX_SUFFIX);
51
+ function browserFromHints(hints) {
52
+ if (hints?.brave) return BRAVE;
53
+ return null;
54
+ }
44
55
  const XBOX_REGEX = new RegExp(XBOX, 'i');
45
56
  const PLAYSTATION_REGEX = new RegExp(PLAYSTATION + ' \\w+', 'i');
46
57
  const NINTENDO_REGEX = new RegExp(NINTENDO + ' \\w+', 'i');
@@ -62,8 +73,10 @@ function isSafari(userAgent) {
62
73
  return includes(userAgent, SAFARI) && !includes(userAgent, CHROME) && !includes(userAgent, ANDROID);
63
74
  }
64
75
  const safariCheck = (ua, vendor)=>vendor && includes(vendor, APPLE) || isSafari(ua);
65
- const detectBrowser = function(user_agent, vendor) {
76
+ const detectBrowser = function(user_agent, vendor, hints) {
66
77
  vendor = vendor || '';
78
+ const fromHints = browserFromHints(hints);
79
+ if (fromHints) return fromHints;
67
80
  if (includes(user_agent, ' OPR/') && includes(user_agent, 'Mini')) return OPERA_MINI;
68
81
  if (includes(user_agent, ' OPR/')) return OPERA;
69
82
  if (BLACKBERRY_REGEX.test(user_agent)) return BLACKBERRY;
@@ -71,6 +84,10 @@ const detectBrowser = function(user_agent, vendor) {
71
84
  if (includes(user_agent, 'OculusBrowser')) return OCULUS_BROWSER;
72
85
  else if (includes(user_agent, SAMSUNG_BROWSER)) return SAMSUNG_INTERNET;
73
86
  else if (includes(user_agent, EDGE) || includes(user_agent, 'Edg/')) return MICROSOFT_EDGE;
87
+ else if (includes(user_agent, VIVALDI + '/')) return VIVALDI;
88
+ else if (includes(user_agent, 'YaBrowser/')) return YANDEX;
89
+ else if (includes(user_agent, WHALE + '/')) return WHALE;
90
+ else if (includes(user_agent, DUCKDUCKGO + '/') || includes(user_agent, 'Ddg/')) return DUCKDUCKGO;
74
91
  else if (includes(user_agent, 'FBIOS')) return FACEBOOK + ' ' + MOBILE;
75
92
  else if (includes(user_agent, 'UCWEB') || includes(user_agent, 'UCBrowser')) return 'UC Browser';
76
93
  else if (includes(user_agent, 'CriOS')) return CHROME_IOS;
@@ -79,7 +96,10 @@ const detectBrowser = function(user_agent, vendor) {
79
96
  else if (includes(user_agent, ANDROID) && includes(user_agent, SAFARI)) return ANDROID_MOBILE;
80
97
  else if (includes(user_agent, 'FxiOS')) return FIREFOX_IOS;
81
98
  else if (includes(user_agent.toLowerCase(), KONQUEROR.toLowerCase())) return KONQUEROR;
99
+ else if (includes(user_agent, BRAVE + '/')) return BRAVE;
82
100
  else if (safariCheck(user_agent, vendor)) return includes(user_agent, MOBILE) ? MOBILE_SAFARI : SAFARI;
101
+ else if (includes(user_agent, 'PaleMoon/')) return PALE_MOON;
102
+ else if (includes(user_agent, WATERFOX + '/')) return WATERFOX;
83
103
  else if (includes(user_agent, FIREFOX)) return FIREFOX;
84
104
  else if (includes(user_agent, 'MSIE') || includes(user_agent, 'Trident/')) return INTERNET_EXPLORER;
85
105
  else if (includes(user_agent, 'Gecko')) return FIREFOX;
@@ -132,6 +152,27 @@ const versionRegexes = {
132
152
  [OCULUS_BROWSER]: [
133
153
  new RegExp('OculusBrowser\\/' + BROWSER_VERSION_REGEX_SUFFIX)
134
154
  ],
155
+ [VIVALDI]: [
156
+ new RegExp(VIVALDI + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)
157
+ ],
158
+ [YANDEX]: [
159
+ new RegExp('YaBrowser\\/' + BROWSER_VERSION_REGEX_SUFFIX)
160
+ ],
161
+ [WHALE]: [
162
+ new RegExp(WHALE + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)
163
+ ],
164
+ [BRAVE]: [
165
+ new RegExp(BRAVE + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)
166
+ ],
167
+ [DUCKDUCKGO]: [
168
+ new RegExp('(DuckDuckGo|Ddg)\\/' + BROWSER_VERSION_REGEX_SUFFIX)
169
+ ],
170
+ [PALE_MOON]: [
171
+ new RegExp('PaleMoon\\/' + BROWSER_VERSION_REGEX_SUFFIX)
172
+ ],
173
+ [WATERFOX]: [
174
+ new RegExp(WATERFOX + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)
175
+ ],
135
176
  [INTERNET_EXPLORER]: [
136
177
  new RegExp('(rv:|MSIE )' + BROWSER_VERSION_REGEX_SUFFIX)
137
178
  ],
@@ -139,8 +180,8 @@ const versionRegexes = {
139
180
  new RegExp('rv:' + BROWSER_VERSION_REGEX_SUFFIX)
140
181
  ]
141
182
  };
142
- const detectBrowserVersion = function(userAgent, vendor) {
143
- const browser = detectBrowser(userAgent, vendor);
183
+ const detectBrowserVersion = function(userAgent, vendor, hints) {
184
+ const browser = detectBrowser(userAgent, vendor, hints);
144
185
  const regexes = versionRegexes[browser];
145
186
  if (isUndefined(regexes)) return null;
146
187
  for(let i = 0; i < regexes.length; i++){
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/core",
3
- "version": "1.29.15",
3
+ "version": "1.30.1",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -67,7 +67,7 @@
67
67
  }
68
68
  },
69
69
  "dependencies": {
70
- "@posthog/types": "1.376.6"
70
+ "@posthog/types": "1.378.0"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@rslib/core": "0.10.6",
@@ -50,10 +50,36 @@ const GENERIC_MOBILE = GENERIC + ' ' + MOBILE.toLowerCase()
50
50
  const GENERIC_TABLET = GENERIC + ' ' + TABLET.toLowerCase()
51
51
  const KONQUEROR = 'Konqueror'
52
52
  const OCULUS_BROWSER = 'Oculus Browser'
53
+ const VIVALDI = 'Vivaldi'
54
+ const YANDEX = 'Yandex'
55
+ const WHALE = 'Whale'
56
+ const DUCKDUCKGO = 'DuckDuckGo'
57
+ const PALE_MOON = 'Pale Moon'
58
+ const WATERFOX = 'Waterfox'
59
+ const BRAVE = 'Brave'
53
60
 
54
61
  const BROWSER_VERSION_REGEX_SUFFIX = '(\\d+(\\.\\d+)?)'
55
62
  const DEFAULT_BROWSER_VERSION_REGEX = new RegExp('Version/' + BROWSER_VERSION_REGEX_SUFFIX)
56
63
 
64
+ /**
65
+ * Hints from sources outside the User-Agent string. These let us identify Brave
66
+ * on desktop / Android — Chromium-based with no UA marker, but it exposes
67
+ * `navigator.brave`. (Brave on iOS is detected via its `Brave/X` UA marker —
68
+ * WebKit doesn't expose `navigator.brave` — so no hint is needed there.)
69
+ */
70
+ export interface BrowserDetectionHints {
71
+ // Set to `true` when `navigator.brave` exists. This is the sync detection
72
+ // signal Brave recommends. Not available on iOS — see UA fallback below.
73
+ brave?: boolean
74
+ }
75
+
76
+ function browserFromHints(hints: BrowserDetectionHints | undefined): string | null {
77
+ if (hints?.brave) {
78
+ return BRAVE
79
+ }
80
+ return null
81
+ }
82
+
57
83
  const XBOX_REGEX = new RegExp(XBOX, 'i')
58
84
  const PLAYSTATION_REGEX = new RegExp(PLAYSTATION + ' \\w+', 'i')
59
85
  const NINTENDO_REGEX = new RegExp(NINTENDO + ' \\w+', 'i')
@@ -88,10 +114,27 @@ const safariCheck = (ua: string, vendor?: string) => (vendor && includes(vendor,
88
114
  * This function detects which browser is running this script.
89
115
  * The order of the checks are important since many user agents
90
116
  * include keywords used in later checks.
117
+ *
118
+ * `hints` is an optional bag of out-of-band signals (`navigator.brave`) used to
119
+ * detect browsers that intentionally do not identify themselves in the UA
120
+ * string. When omitted, only UA-string detection runs — preserving the previous
121
+ * behaviour.
91
122
  */
92
- export const detectBrowser = function (user_agent: string, vendor: string | undefined): string {
123
+ export const detectBrowser = function (
124
+ user_agent: string,
125
+ vendor: string | undefined,
126
+ hints?: BrowserDetectionHints
127
+ ): string {
93
128
  vendor = vendor || '' // vendor is undefined for at least IE9
94
129
 
130
+ // Out-of-band signals win over UA sniffing because desktop Brave is
131
+ // deliberately invisible in the UA string and would otherwise be
132
+ // misdetected as Chrome.
133
+ const fromHints = browserFromHints(hints)
134
+ if (fromHints) {
135
+ return fromHints
136
+ }
137
+
95
138
  if (includes(user_agent, ' OPR/') && includes(user_agent, 'Mini')) {
96
139
  return OPERA_MINI
97
140
  } else if (includes(user_agent, ' OPR/')) {
@@ -113,6 +156,17 @@ export const detectBrowser = function (user_agent: string, vendor: string | unde
113
156
  return SAMSUNG_INTERNET
114
157
  } else if (includes(user_agent, EDGE) || includes(user_agent, 'Edg/')) {
115
158
  return MICROSOFT_EDGE
159
+ }
160
+ // Chromium forks that DO stamp themselves into the UA. These must be
161
+ // checked before Chrome because their UA also contains `Chrome/`.
162
+ else if (includes(user_agent, VIVALDI + '/')) {
163
+ return VIVALDI
164
+ } else if (includes(user_agent, 'YaBrowser/')) {
165
+ return YANDEX
166
+ } else if (includes(user_agent, WHALE + '/')) {
167
+ return WHALE
168
+ } else if (includes(user_agent, DUCKDUCKGO + '/') || includes(user_agent, 'Ddg/')) {
169
+ return DUCKDUCKGO
116
170
  } else if (includes(user_agent, 'FBIOS')) {
117
171
  return FACEBOOK + ' ' + MOBILE
118
172
  } else if (includes(user_agent, 'UCWEB') || includes(user_agent, 'UCBrowser')) {
@@ -129,8 +183,21 @@ export const detectBrowser = function (user_agent: string, vendor: string | unde
129
183
  return FIREFOX_IOS
130
184
  } else if (includes(user_agent.toLowerCase(), KONQUEROR.toLowerCase())) {
131
185
  return KONQUEROR
186
+ }
187
+ // Brave on iOS does stamp itself into the UA as `Brave/X` — desktop and
188
+ // Android Brave intentionally do not. Must come before the Safari branch
189
+ // because iOS Brave's UA otherwise looks like Mobile Safari.
190
+ else if (includes(user_agent, BRAVE + '/')) {
191
+ return BRAVE
132
192
  } else if (safariCheck(user_agent, vendor)) {
133
193
  return includes(user_agent, MOBILE) ? MOBILE_SAFARI : SAFARI
194
+ }
195
+ // Firefox forks that stamp themselves into the UA. Must precede the
196
+ // generic Firefox check because they also include `Firefox/` (or `Gecko`).
197
+ else if (includes(user_agent, 'PaleMoon/')) {
198
+ return PALE_MOON
199
+ } else if (includes(user_agent, WATERFOX + '/')) {
200
+ return WATERFOX
134
201
  } else if (includes(user_agent, FIREFOX)) {
135
202
  return FIREFOX
136
203
  } else if (includes(user_agent, 'MSIE') || includes(user_agent, 'Trident/')) {
@@ -159,6 +226,17 @@ const versionRegexes: Record<string, RegExp[]> = {
159
226
  [ANDROID_MOBILE]: [new RegExp('android\\s' + BROWSER_VERSION_REGEX_SUFFIX, 'i')],
160
227
  [SAMSUNG_INTERNET]: [new RegExp(SAMSUNG_BROWSER + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
161
228
  [OCULUS_BROWSER]: [new RegExp('OculusBrowser\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
229
+ [VIVALDI]: [new RegExp(VIVALDI + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
230
+ [YANDEX]: [new RegExp('YaBrowser\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
231
+ [WHALE]: [new RegExp(WHALE + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
232
+ // Brave on iOS exposes itself as `Brave/X.X` in the UA. Desktop / Android
233
+ // Brave don't, which is why hint-based Brave detection returns a null
234
+ // version: we have no UA marker to parse.
235
+ [BRAVE]: [new RegExp(BRAVE + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
236
+ // DuckDuckGo on iOS uses `Ddg/`, on Android/desktop preview it uses `DuckDuckGo/`.
237
+ [DUCKDUCKGO]: [new RegExp('(DuckDuckGo|Ddg)\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
238
+ [PALE_MOON]: [new RegExp('PaleMoon\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
239
+ [WATERFOX]: [new RegExp(WATERFOX + '\\/' + BROWSER_VERSION_REGEX_SUFFIX)],
162
240
  [INTERNET_EXPLORER]: [new RegExp('(rv:|MSIE )' + BROWSER_VERSION_REGEX_SUFFIX)],
163
241
  Mozilla: [new RegExp('rv:' + BROWSER_VERSION_REGEX_SUFFIX)],
164
242
  }
@@ -171,8 +249,16 @@ const versionRegexes: Record<string, RegExp[]> = {
171
249
  * `navigator.vendor` is passed in and used to help with detecting certain browsers
172
250
  * NB `navigator.vendor` is deprecated and not present in every browser
173
251
  */
174
- export const detectBrowserVersion = function (userAgent: string, vendor: string | undefined): number | null {
175
- const browser = detectBrowser(userAgent, vendor)
252
+ export const detectBrowserVersion = function (
253
+ userAgent: string,
254
+ vendor: string | undefined,
255
+ hints?: BrowserDetectionHints
256
+ ): number | null {
257
+ const browser = detectBrowser(userAgent, vendor, hints)
258
+
259
+ // Desktop / Android Brave has no parseable UA version, so it returns null
260
+ // below: its `versionRegexes` entry only matches the iOS `Brave/` marker
261
+ // (absent from a desktop Chrome UA).
176
262
  const regexes: RegExp[] | undefined = versionRegexes[browser as keyof typeof versionRegexes]
177
263
  if (isUndefined(regexes)) {
178
264
  return null