-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuserAgent.ts
More file actions
72 lines (65 loc) · 2.7 KB
/
Copy pathuserAgent.ts
File metadata and controls
72 lines (65 loc) · 2.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import type { SessionDevice } from "./types.js";
/**
* Minimal user agent parsing: enough to let the user tell one of their browsers from another
* in the sessions list. A full UA database is not worth a dependency for that.
* Order matters in both lists: every browser below pretends to be the ones after it.
*/
const BROWSERS: { name: string, re: RegExp }[] = [
{ name: 'Edge', re: /Edg(?:e|A|iOS)?\/(\d+)/ },
{ name: 'Opera', re: /(?:OPR|OPiOS|Opera)\/(\d+)/ },
{ name: 'Samsung Internet', re: /SamsungBrowser\/(\d+)/ },
{ name: 'Vivaldi', re: /Vivaldi\/(\d+)/ },
{ name: 'Firefox', re: /(?:Firefox|FxiOS)\/(\d+)/ },
{ name: 'Chrome', re: /(?:Chrome|CriOS)\/(\d+)/ },
// safari on ios puts a `Mobile/<build>` token between its version and the Safari one.
// Minor version must start with a dot, so the major one has a single way to match the digits:
// `(\d+)[\d.]*` backtracks quadratically on a long run of digits
{ name: 'Safari', re: /Version\/(\d+)(?:\.[\d.]*)?(?: Mobile\/\S+)? Safari/ },
];
const OSES: { name: string, re: RegExp }[] = [
{ name: 'Windows', re: /Windows NT [\d.]+/ },
{ name: 'ChromeOS', re: /CrOS/ },
{ name: 'Android', re: /Android (\d+)/ },
// iPhone and iPod send `CPU iPhone OS 18_1`, iPad sends `CPU OS 18_1`. Anchoring on that token
// instead of `(?:iPhone|iPad|iPod).* OS` avoids rescanning the rest of the string for every device name
{ name: 'iOS', re: /CPU (?:iPhone )?OS (\d+)/ },
{ name: 'macOS', re: /Mac OS X/ },
{ name: 'Linux', re: /Linux|X11/ },
];
const TABLET_RE = /iPad|Tablet|PlayBook|Silk|Android(?!.*Mobile)/;
const MOBILE_RE = /Mobi|iPhone|iPod|Windows Phone|Android/;
/**
* Appends the major version the regex captured, e.g. `Chrome 131` or `iOS 18`.
*/
function named(name: string, match: RegExpExecArray): string {
return match[1] ? `${name} ${match[1]}` : name;
}
function firstMatch(userAgent: string, candidates: { name: string, re: RegExp }[]): string | null {
for (const { name, re } of candidates) {
const match = re.exec(userAgent);
if (match) {
return named(name, match);
}
}
return null;
}
function deviceType(userAgent: string): SessionDevice['type'] {
if (TABLET_RE.test(userAgent)) {
return 'tablet';
}
return MOBILE_RE.test(userAgent) ? 'mobile' : 'desktop';
}
/**
* Returns null when there is nothing to parse: sessions created before this plugin started storing
* the user agent, and clients which send no `User-Agent` header at all.
*/
export function parseUserAgent(userAgent: string | null | undefined): SessionDevice | null {
if (!userAgent) {
return null;
}
return {
browser: firstMatch(userAgent, BROWSERS),
os: firstMatch(userAgent, OSES),
type: deviceType(userAgent),
};
}