Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions packages/util-user-agent-browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,27 @@
[![NPM version](https://img.shields.io/npm/v/@aws-sdk/util-user-agent-browser/latest.svg)](https://www.npmjs.com/package/@aws-sdk/util-user-agent-browser)
[![NPM downloads](https://img.shields.io/npm/dm/@aws-sdk/util-user-agent-browser.svg)](https://www.npmjs.com/package/@aws-sdk/util-user-agent-browser)

> An internal package

## Usage

You probably shouldn't, at least directly.
In previous versions of the AWS SDK for JavaScript v3, the AWS SDK user agent header was provided by parsing the navigator user agent string with the `bowser` library.

This was later changed to browser feature detection using the native Navigator APIs, but if you would like to have the previous functionality, use the following code:

```js
import { createUserAgentStringParsingProvider } from "@aws-sdk/util-user-agent-browser";

import { S3Client } from "@aws-sdk/client-s3";
import pkgInfo from "@aws-sdk/client-s3/package.json";
// or any other client.

const client = new S3Client({
defaultUserAgentProvider: createUserAgentStringParsingProvider({
// For a client's serviceId, check the corresponding shared runtimeConfig file
// https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/src/runtimeConfig.shared.ts
serviceId: "S3",
clientVersion: pkgInfo.version,
}),
});
```

This usage is not recommended, due to the size of the additional parsing library.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest";

import { createUserAgentStringParsingProvider } from "./createUserAgentStringParsingProvider";
import type { PreviouslyResolved } from "./index";

const ua =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36";

const mockConfig: PreviouslyResolved = {
userAgentAppId: vi.fn().mockResolvedValue(undefined),
};

describe("createUserAgentStringParsingProvider", () => {
beforeEach(() => {
vi.spyOn(window.navigator, "userAgent", "get").mockReturnValue(ua);
});

afterEach(() => {
vi.clearAllMocks();
});

it("should populate metrics", async () => {
const userAgent = await createUserAgentStringParsingProvider({ serviceId: "s3", clientVersion: "0.1.0" })(
mockConfig
);
expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]);
expect(userAgent[1]).toEqual(["ua", "2.1"]);
expect(userAgent[2]).toEqual(["os/macOS", "10.15.7"]);
expect(userAgent[3]).toEqual(["lang/js"]);
expect(userAgent[4]).toEqual(["md/browser", "Chrome_86.0.4240.111"]);
expect(userAgent[5]).toEqual(["api/s3", "0.1.0"]);
expect(userAgent.length).toBe(6);
});

it("should populate metrics when service id not available", async () => {
const userAgent = await createUserAgentStringParsingProvider({ serviceId: undefined, clientVersion: "0.1.0" })(
mockConfig
);
expect(userAgent).not.toContainEqual(["api/s3", "0.1.0"]);
expect(userAgent.length).toBe(5);
});

it("should include appId when provided", async () => {
const configWithAppId: PreviouslyResolved = {
userAgentAppId: vi.fn().mockResolvedValue("test-app-id"),
};
const userAgent = await createUserAgentStringParsingProvider({ serviceId: "s3", clientVersion: "0.1.0" })(
configWithAppId
);
expect(userAgent[6]).toEqual(["app/test-app-id"]);
expect(userAgent.length).toBe(7);
});

it("should not include appId when not provided", async () => {
const userAgent = await createUserAgentStringParsingProvider({ serviceId: "s3", clientVersion: "0.1.0" })(
mockConfig
);
expect(userAgent).not.toContainEqual(expect.arrayContaining(["app/"]));
expect(userAgent.length).toBe(6);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { UserAgent } from "@smithy/types";

import type { DefaultUserAgentOptions } from "./configurations";
import type { PreviouslyResolved } from "./index";

/**
* This is an alternative to the default user agent provider that uses the bowser
* library to parse the user agent string.
*
* Use this with your client's `defaultUserAgentProvider` constructor object field
* to use the legacy behavior.
*
* @deprecated use the default provider unless you need the older UA-parsing functionality.
* @public
*/
export const createUserAgentStringParsingProvider =
({ serviceId, clientVersion }: DefaultUserAgentOptions): ((config?: PreviouslyResolved) => Promise<UserAgent>) =>
async (config?: PreviouslyResolved) => {
const module = await import("bowser");
const parse = module.parse ?? module.default.parse ?? (() => "");

const parsedUA =
typeof window !== "undefined" && window?.navigator?.userAgent ? parse(window.navigator.userAgent) : undefined;
const sections: UserAgent = [
// sdk-metadata
["aws-sdk-js", clientVersion],
// ua-metadata
["ua", "2.1"],
// os-metadata
[`os/${parsedUA?.os?.name || "other"}`, parsedUA?.os?.version],
// language-metadata
// ECMAScript edition doesn't matter in JS.
["lang/js"],
// browser vendor and version.
["md/browser", `${parsedUA?.browser?.name ?? "unknown"}_${parsedUA?.browser?.version ?? "unknown"}`],
];

if (serviceId) {
// api-metadata
// service Id may not appear in non-AWS clients
sections.push([`api/${serviceId}`, clientVersion]);
}

const appId = await config?.userAgentAppId?.();
if (appId) {
sections.push([`app/${appId}`]);
}

return sections;
};
7 changes: 4 additions & 3 deletions packages/util-user-agent-browser/src/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { Provider, UserAgent } from "@smithy/types";

import { DefaultUserAgentOptions } from "./configurations";

/**
* @internal
*/
export interface PreviouslyResolved {
userAgentAppId: Provider<string | undefined>;
}

/**
* Default provider to the user agent in ReactNative.
* @internal
*
* Default provider to the user agent in ReactNative. It's a best effort to infer
* the device information. It uses bowser library to detect the browser and virsion
*/
export const createDefaultUserAgentProvider =
({ serviceId, clientVersion }: DefaultUserAgentOptions): ((config?: PreviouslyResolved) => Promise<UserAgent>) =>
Expand Down
138 changes: 98 additions & 40 deletions packages/util-user-agent-browser/src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,114 @@
import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest";
import { describe, expect, test as it, vi } from "vitest";

import { createDefaultUserAgentProvider, PreviouslyResolved } from ".";
import { createDefaultUserAgentProvider, fallback } from "./index";

const ua =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36";

const mockConfig: PreviouslyResolved = {
userAgentAppId: vi.fn().mockResolvedValue(undefined),
type NavigatorTestAugment = {
userAgentData?: {
brands?: {
brand?: string;
version?: string;
}[];
platform?: string;
};
};

describe("createDefaultUserAgentProvider", () => {
beforeEach(() => {
vi.spyOn(window.navigator, "userAgent", "get").mockReturnValue(ua);
});
describe("browser and os brand detection", () => {
it("should use navigator.userAgentData when available", async () => {
const navigator = window.navigator as typeof window.navigator & NavigatorTestAugment;
navigator.userAgentData = {
platform: "linux",
brands: [
{ brand: "test", version: "1" },
{ brand: "BROWSER_BRAND", version: "10000" },
],
};

afterEach(() => {
vi.clearAllMocks();
});
const uaProvider = createDefaultUserAgentProvider({
serviceId: "AWS",
clientVersion: "3.0.0",
});

it("should populate metrics", async () => {
const userAgent = await createDefaultUserAgentProvider({ serviceId: "s3", clientVersion: "0.1.0" })(mockConfig);
expect(userAgent[0]).toEqual(["aws-sdk-js", "0.1.0"]);
expect(userAgent[1]).toEqual(["ua", "2.1"]);
expect(userAgent[2]).toEqual(["os/macOS", "10.15.7"]);
expect(userAgent[3]).toEqual(["lang/js"]);
expect(userAgent[4]).toEqual(["md/browser", "Chrome_86.0.4240.111"]);
expect(userAgent[5]).toEqual(["api/s3", "0.1.0"]);
expect(userAgent.length).toBe(6);
const sdkUa = await uaProvider();
expect(sdkUa.flatMap((_) => _.filter(Boolean).join("#")).join(" ")).toEqual(
"aws-sdk-js#3.0.0 ua#2.1 os/linux lang/js md/browser#BROWSER_BRAND_10000 api/AWS#3.0.0"
);
});

it("should populate metrics when service id not available", async () => {
const userAgent = await createDefaultUserAgentProvider({ serviceId: undefined, clientVersion: "0.1.0" })(
mockConfig
it("falls back to basic user agent parsing", async () => {
const navigator = window.navigator as typeof window.navigator & NavigatorTestAugment;
delete navigator.userAgentData;

vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Linux Firefox/");

const uaProvider = createDefaultUserAgentProvider({
serviceId: "AWS",
clientVersion: "3.0.0",
});

const sdkUa = await uaProvider();
expect(sdkUa.flatMap((_) => _.filter(Boolean).join("#")).join(" ")).toEqual(
"aws-sdk-js#3.0.0 ua#2.1 os/Linux lang/js md/browser#Firefox_unknown api/AWS#3.0.0"
);
expect(userAgent).not.toContainEqual(["api/s3", "0.1.0"]);
expect(userAgent.length).toBe(5);
});

it("should include appId when provided", async () => {
const configWithAppId: PreviouslyResolved = {
userAgentAppId: vi.fn().mockResolvedValue("test-app-id"),
};
const userAgent = await createDefaultUserAgentProvider({ serviceId: "s3", clientVersion: "0.1.0" })(
configWithAppId
it("uses defaults when unable to detect any specific OS or browser brand", async () => {
const navigator = window.navigator as typeof window.navigator & NavigatorTestAugment;
delete navigator.userAgentData;

vi.spyOn(navigator, "userAgent", "get").mockReturnValue("");

const uaProvider = createDefaultUserAgentProvider({
serviceId: "AWS",
clientVersion: "3.0.0",
});

const sdkUa = await uaProvider();
expect(sdkUa.flatMap((_) => _.filter(Boolean).join("#")).join(" ")).toEqual(
"aws-sdk-js#3.0.0 ua#2.1 os/other lang/js md/browser#unknown_unknown api/AWS#3.0.0"
);
expect(userAgent[6]).toEqual(["app/test-app-id"]);
expect(userAgent.length).toBe(7);
});
});

describe("ua fallback parsing", () => {
const samples = [
`Mozilla/5.0 (Macintosh; Intel Mac OS X 15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15`,
`Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6.2 Mobile/15E148 Safari/604.1`,
`Mozilla/5.0 (iPad; CPU OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6.2 Mobile/15E148 Safari/604.1`,
`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36`,
`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36`,
`Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36`,
`Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.44 Mobile Safari/537.36`,
`Mozilla/5.0 (Linux; Android 16; LM-Q710(FGN)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.44 Mobile Safari/537.36`,
`Mozilla/5.0 (Android 16; Mobile; LG-M255; rv:143.0) Gecko/143.0 Firefox/143.0`,
`Mozilla/5.0 (Android 16; Mobile; rv:68.0) Gecko/68.0 Firefox/143.0`,
`Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 EdgiOS/140.3485.94 Mobile/15E148 Safari/605.1.15`,
`Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.7390.44 Mobile Safari/537.36 EdgA/140.0.3485.98`,
`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.3537.57`,
`Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.3537.57`,
`Mozilla/5.0 (X11; Linux i686; rv:143.0) Gecko/20100101 Firefox/143.0`,
];

const expectedBrands = [
["macOS", "Safari"],
["iOS", "Safari"],
["iOS", "Safari"],
["macOS", "Chrome"],
["Windows", "Chrome"],
["Linux", "Chrome"],
["Android", "Chrome"],
["Android", "Chrome"],
["Android", "Firefox"],
["Android", "Firefox"],
["iOS", "Microsoft Edge"],
["Android", "Microsoft Edge"],
["macOS", "Microsoft Edge"],
["Windows", "Microsoft Edge"],
["Linux", "Firefox"],
];

it("should not include appId when not provided", async () => {
const userAgent = await createDefaultUserAgentProvider({ serviceId: "s3", clientVersion: "0.1.0" })(mockConfig);
expect(userAgent).not.toContainEqual(expect.arrayContaining(["app/"]));
expect(userAgent.length).toBe(6);
it("should detect os and browser", () => {
for (let i = 0; i < expectedBrands.length; ++i) {
expect([fallback.os(samples[i]), fallback.browser(samples[i])]).toEqual(expectedBrands[i]);
}
});
});
Loading
Loading