Skip to content

Commit c1544be

Browse files
authored
feat: use feature detection for defaults-mode-browser (#1730)
1 parent 300177f commit c1544be

File tree

5 files changed

+84
-26
lines changed

5 files changed

+84
-26
lines changed

.changeset/sixty-cows-pretend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/util-defaults-mode-browser": minor
3+
---
4+
5+
remove bower from mobile device detection

packages/util-defaults-mode-browser/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
"@smithy/property-provider": "workspace:^",
2828
"@smithy/smithy-client": "workspace:^",
2929
"@smithy/types": "workspace:^",
30-
"bowser": "^2.11.0",
3130
"tslib": "^2.6.2"
3231
},
3332
"devDependencies": {

packages/util-defaults-mode-browser/src/resolveDefaultsModeConfig.spec.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,37 @@
1-
import bowser from "bowser";
2-
import { afterEach, describe, expect, test as it, vi } from "vitest";
1+
import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest";
32

43
import { DEFAULTS_MODE_OPTIONS } from "./constants";
54
import { resolveDefaultsModeConfig } from "./resolveDefaultsModeConfig";
6-
vi.mock("bowser");
5+
6+
/**
7+
* @internal
8+
*/
9+
type NavigatorTestAugment = Navigator & {
10+
userAgentData?: {
11+
mobile?: boolean;
12+
};
13+
connection?: {
14+
effectiveType?: "4g" | string;
15+
rtt?: number;
16+
downlink?: number;
17+
};
18+
};
719

820
describe("resolveDefaultsModeConfig", () => {
921
const uaSpy = vi.spyOn(window.navigator, "userAgent", "get").mockReturnValue("some UA");
1022

23+
beforeEach(() => {
24+
const navigator = window.navigator as NavigatorTestAugment;
25+
if (!navigator.userAgentData || !navigator.connection) {
26+
navigator.userAgentData = {};
27+
navigator.connection = {};
28+
}
29+
});
30+
1131
afterEach(() => {
32+
const navigator = window.navigator as NavigatorTestAugment;
33+
delete navigator.userAgentData;
34+
delete navigator.connection;
1235
uaSpy.mockClear();
1336
});
1437

@@ -22,17 +45,20 @@ describe("resolveDefaultsModeConfig", () => {
2245
});
2346

2447
it("should resolve auto mode to mobile if platform is mobile", async () => {
25-
(bowser.parse as any).mockReturnValue({ platform: { type: "mobile" } });
48+
vi.spyOn(window.navigator as NavigatorTestAugment, "userAgentData", "get").mockReturnValue({
49+
mobile: true,
50+
});
2651
expect(await resolveDefaultsModeConfig({ defaultsMode: () => Promise.resolve("auto") })()).toBe("mobile");
2752
});
2853

29-
it("should resolve auto mode to mobile if platform is tablet", async () => {
30-
(bowser.parse as any).mockReturnValue({ platform: { type: "tablet" } });
54+
it("should resolve auto mode to mobile if connection is not 4g (5g is not possible in this enum)", async () => {
55+
vi.spyOn(window.navigator as NavigatorTestAugment, "connection", "get").mockReturnValue({
56+
effectiveType: "3g",
57+
});
3158
expect(await resolveDefaultsModeConfig({ defaultsMode: () => Promise.resolve("auto") })()).toBe("mobile");
3259
});
3360

3461
it("should resolve auto mode to standard if platform not mobile or tablet", async () => {
35-
(bowser.parse as any).mockReturnValue({ platform: { type: "desktop" } });
3662
expect(await resolveDefaultsModeConfig({ defaultsMode: () => Promise.resolve("auto") })()).toBe("standard");
3763
});
3864

packages/util-defaults-mode-browser/src/resolveDefaultsModeConfig.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { memoize } from "@smithy/property-provider";
22
import type { DefaultsMode, ResolvedDefaultsMode } from "@smithy/smithy-client";
33
import type { Provider } from "@smithy/types";
4-
import bowser from "bowser";
54

65
import { DEFAULTS_MODE_OPTIONS } from "./constants";
76

@@ -27,7 +26,7 @@ export const resolveDefaultsModeConfig = ({
2726
const mode = typeof defaultsMode === "function" ? await defaultsMode() : defaultsMode;
2827
switch (mode?.toLowerCase()) {
2928
case "auto":
30-
return Promise.resolve(isMobileBrowser() ? "mobile" : "standard");
29+
return Promise.resolve(useMobileConfiguration() ? "mobile" : "standard");
3130
case "mobile":
3231
case "in-region":
3332
case "cross-region":
@@ -43,12 +42,49 @@ export const resolveDefaultsModeConfig = ({
4342
}
4443
});
4544

46-
const isMobileBrowser = (): boolean => {
47-
const parsedUA =
48-
typeof window !== "undefined" && window?.navigator?.userAgent
49-
? bowser.parse(window.navigator.userAgent)
50-
: undefined;
51-
const platform = parsedUA?.platform?.type;
52-
// Reference: https://github.com/lancedikson/bowser/blob/master/src/constants.js#L86
53-
return platform === "tablet" || platform === "mobile";
45+
/**
46+
* @internal
47+
*/
48+
type NavigatorAugment = {
49+
userAgentData?: {
50+
mobile?: boolean;
51+
};
52+
connection?: {
53+
effectiveType?: "4g" | string;
54+
rtt?: number;
55+
downlink?: number;
56+
};
57+
};
58+
59+
/**
60+
* The aim of the mobile detection function is not really to know whether the device is a mobile device.
61+
* This is emphasized in the modern guidance on browser detection that feature detection is correct
62+
* whereas UA "sniffing" is usually a mistake.
63+
*
64+
* So then, the underlying reason we are trying to detect a mobile device is not for any particular device feature,
65+
* but rather the implied network speed available to the program (we use it to set a default request timeout value).
66+
*
67+
* Therefore, it is better to use network speed related feature detection when available. This also saves
68+
* 20kb (minified) from the bowser dependency we were using.
69+
*
70+
* @internal
71+
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Browser_detection_using_the_user_agent
72+
*/
73+
const useMobileConfiguration = (): boolean => {
74+
const navigator = window?.navigator as (typeof window.navigator & NavigatorAugment) | undefined;
75+
if (navigator?.connection) {
76+
// https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/effectiveType
77+
// The maximum will report as 4g, regardless of 5g or further developments.
78+
const { effectiveType, rtt, downlink } = navigator?.connection;
79+
const slow =
80+
(typeof effectiveType === "string" && effectiveType !== "4g") || Number(rtt) > 100 || Number(downlink) < 10;
81+
if (slow) {
82+
return true;
83+
}
84+
}
85+
86+
// without the networkInformation object, we use the userAgentData or touch feature detection as a proxy.
87+
return (
88+
navigator?.userAgentData?.mobile || (typeof navigator?.maxTouchPoints === "number" && navigator?.maxTouchPoints > 1)
89+
);
5490
};

yarn.lock

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3286,7 +3286,6 @@ __metadata:
32863286
"@smithy/smithy-client": "workspace:^"
32873287
"@smithy/types": "workspace:^"
32883288
"@types/node": "npm:^18.11.9"
3289-
bowser: "npm:^2.11.0"
32903289
concurrently: "npm:7.0.0"
32913290
downlevel-dts: "npm:0.10.1"
32923291
rimraf: "npm:3.0.2"
@@ -4655,13 +4654,6 @@ __metadata:
46554654
languageName: node
46564655
linkType: hard
46574656

4658-
"bowser@npm:^2.11.0":
4659-
version: 2.11.0
4660-
resolution: "bowser@npm:2.11.0"
4661-
checksum: 10c0/04efeecc7927a9ec33c667fa0965dea19f4ac60b3fea60793c2e6cf06c1dcd2f7ae1dbc656f450c5f50783b1c75cf9dc173ba6f3b7db2feee01f8c4b793e1bd3
4662-
languageName: node
4663-
linkType: hard
4664-
46654657
"brace-expansion@npm:^1.1.7":
46664658
version: 1.1.11
46674659
resolution: "brace-expansion@npm:1.1.11"

0 commit comments

Comments
 (0)