Skip to content

Commit

Permalink
[core] Adding React-Native user agent platforms (#30083)
Browse files Browse the repository at this point in the history
### Packages impacted by this PR

- @azure/core-rest-pipeline
- @typespec/ts-http-runtime

### Issues associated with this PR

- #30065

### Describe the problem that is addressed by this PR

Adds more support for user agents for browser and fixes React-Native
implementation to import "react-native".

### What are the possible designs available to address the problem? If
there are more than one possible design, why was the one in this PR
chosen?


### Are there test cases added in this PR? _(If not, why?)_


### Provide a list of related PRs _(if any)_


### Command used to generate this PR:**_(Applicable only to SDK release
request PRs)_

### Checklists
- [ ] Added impacted package name to the issue description
- [ ] Does this PR needs any fixes in the SDK Generator?** _(If so,
create an Issue in the
[Autorest/typescript](https://github.com/Azure/autorest.typescript)
repository and link it here)_
- [ ] Added a changelog (if necessary)
  • Loading branch information
mpodwysocki authored Jun 18, 2024
1 parent 57d1bd9 commit 6c754ae
Show file tree
Hide file tree
Showing 17 changed files with 111 additions and 52 deletions.
4 changes: 3 additions & 1 deletion sdk/core/core-rest-pipeline/src/policies/tracingPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export interface TracingPolicyOptions {
* @param options - Options to configure the telemetry logged by the tracing policy.
*/
export function tracingPolicy(options: TracingPolicyOptions = {}): PipelinePolicy {
const userAgent = getUserAgentValue(options.userAgentPrefix);
const userAgentPromise = getUserAgentValue(options.userAgentPrefix);
const sanitizer = new Sanitizer({
additionalAllowedQueryParameters: options.additionalAllowedQueryParameters,
});
Expand All @@ -58,6 +58,8 @@ export function tracingPolicy(options: TracingPolicyOptions = {}): PipelinePolic
return next(request);
}

const userAgent = await userAgentPromise;

const spanAttributes = {
"http.url": sanitizer.sanitizeUrl(request.url),
"http.method": request.method,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function userAgentPolicy(options: UserAgentPolicyOptions = {}): PipelineP
name: userAgentPolicyName,
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
if (!request.headers.has(UserAgentHeaderName)) {
request.headers.set(UserAgentHeaderName, userAgentValue);
request.headers.set(UserAgentHeaderName, await userAgentValue);
}
return next(request);
},
Expand Down
4 changes: 2 additions & 2 deletions sdk/core/core-rest-pipeline/src/util/userAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export function getUserAgentHeaderName(): string {
/**
* @internal
*/
export function getUserAgentValue(prefix?: string): string {
export async function getUserAgentValue(prefix?: string): Promise<string> {
const runtimeInfo = new Map<string, string>();
runtimeInfo.set("core-rest-pipeline", SDK_VERSION);
setPlatformSpecificData(runtimeInfo);
await setPlatformSpecificData(runtimeInfo);
const defaultAgent = getUserAgentString(runtimeInfo);
const userAgentValue = prefix ? `${prefix} ${defaultAgent}` : defaultAgent;
return userAgentValue;
Expand Down
35 changes: 27 additions & 8 deletions sdk/core/core-rest-pipeline/src/util/userAgentPlatform-browser.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,39 @@ export function getHeaderName(): string {

interface NavigatorEx extends Navigator {
userAgentData?: {
brands: { brand: string; version: string }[];
mobile: boolean;
platform?: string;
getHighEntropyValues: (hints: string[]) => Promise<{
architecture: string;
bitness: string;
brands: { brand: string; version: string }[];
formFactor: string;
fullVersionList: { brand: string; version: string }[];
mobile: boolean;
model: string;
platform: string;
platformVersion: string;
wow64: boolean;
}>;
};
}

/**
* @internal
*/
export function setPlatformSpecificData(map: Map<string, string>): void {
export async function setPlatformSpecificData(map: Map<string, string>): Promise<void> {
const localNavigator = globalThis.navigator as NavigatorEx;
map.set(
"OS",
(localNavigator?.userAgentData?.platform ?? localNavigator?.platform ?? "unknown").replace(
" ",
"",
),
);
let osPlatform = "unknown";
if (localNavigator.userAgentData) {
const entropyValues = await localNavigator.userAgentData.getHighEntropyValues([
"architecture",
"platformVersion",
]);
osPlatform = `${entropyValues.architecture}-${entropyValues.platform}-${entropyValues.platformVersion}`;
} else if (localNavigator?.platform) {
osPlatform = localNavigator.platform;
}

map.set("OS", osPlatform);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

const { Platform } = await import("react-native");
import { Platform } from "react-native";

/**
* @internal
Expand All @@ -13,7 +13,7 @@ export function getHeaderName(): string {
/**
* @internal
*/
export function setPlatformSpecificData(map: Map<string, string>): void {
export async function setPlatformSpecificData(map: Map<string, string>): Promise<void> {
if (Platform.constants?.reactNativeVersion) {
const { major, minor, patch } = Platform.constants.reactNativeVersion;
map.set("react-native", `${major}.${minor}.${patch}`);
Expand Down
2 changes: 1 addition & 1 deletion sdk/core/core-rest-pipeline/src/util/userAgentPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function getHeaderName(): string {
/**
* @internal
*/
export function setPlatformSpecificData(map: Map<string, string>): void {
export async function setPlatformSpecificData(map: Map<string, string>): Promise<void> {
if (process && process.versions) {
const versions = process.versions as ExtendedPlatformVersions;
if (versions.bun) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { describe, it, assert } from "vitest";
import { setPlatformSpecificData } from "../../src/util/userAgentPlatform.js";

describe("userAgentPlatform", () => {
it("should set OS", () => {
it("should set OS", async () => {
const map = new Map<string, string>();

setPlatformSpecificData(map);
await setPlatformSpecificData(map);

assert.ok(map.has("OS"));
});
Expand Down
17 changes: 9 additions & 8 deletions sdk/core/core-rest-pipeline/test/node/userAgentPlatform.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,23 @@ describe("userAgentPlatform", () => {
vi.clearAllMocks();
});

it("should handle an empty process.versions", () => {
it("should handle an empty process.versions", async () => {
vi.mocked(process).versions = undefined;
const map = new Map<string, string>();

setPlatformSpecificData(map);
await setPlatformSpecificData(map);

assert.ok(map.has("OS"));
assert.notOk(map.has("Node"));
assert.notOk(map.has("Deno"));
assert.notOk(map.has("Bun"));
});

it("should handle a Node.js process.versions with Bun", () => {
it("should handle a Node.js process.versions with Bun", async () => {
vi.mocked(process).versions = { bun: "1.0.0" };
const map = new Map<string, string>();

setPlatformSpecificData(map);
await setPlatformSpecificData(map);

assert.ok(map.has("OS"));
assert.ok(map.has("Bun"));
Expand All @@ -42,11 +43,11 @@ describe("userAgentPlatform", () => {
assert.notOk(map.has("Deno"));
});

it("should handle a Node.js process.versions with Deno", () => {
it("should handle a Node.js process.versions with Deno", async () => {
vi.mocked(process).versions = { deno: "2.0.0" };
const map = new Map<string, string>();

setPlatformSpecificData(map);
await setPlatformSpecificData(map);

assert.ok(map.has("OS"));
assert.ok(map.has("Deno"));
Expand All @@ -55,11 +56,11 @@ describe("userAgentPlatform", () => {
assert.notOk(map.has("Bun"));
});

it("should handle a Node.js process.versions", () => {
it("should handle a Node.js process.versions", async () => {
vi.mocked(process).versions = { node: "20.0.0" };
const map = new Map<string, string>();

setPlatformSpecificData(map);
await setPlatformSpecificData(map);

assert.ok(map.has("OS"));
assert.ok(map.has("Node"));
Expand Down
4 changes: 3 additions & 1 deletion sdk/core/ts-http-runtime/src/policies/tracingPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface TracingPolicyOptions {
* @param options - Options to configure the telemetry logged by the tracing policy.
*/
export function tracingPolicy(options: TracingPolicyOptions = {}): PipelinePolicy {
const userAgent = getUserAgentValue(options.userAgentPrefix);
const userAgentValue = getUserAgentValue(options.userAgentPrefix);
const sanitizer = new Sanitizer({
additionalAllowedQueryParameters: options.additionalAllowedQueryParameters,
});
Expand All @@ -54,6 +54,8 @@ export function tracingPolicy(options: TracingPolicyOptions = {}): PipelinePolic
return next(request);
}

const userAgent = await userAgentValue;

const spanAttributes = {
"http.url": sanitizer.sanitizeUrl(request.url),
"http.method": request.method,
Expand Down
2 changes: 1 addition & 1 deletion sdk/core/ts-http-runtime/src/policies/userAgentPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function userAgentPolicy(options: UserAgentPolicyOptions = {}): PipelineP
name: userAgentPolicyName,
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
if (!request.headers.has(UserAgentHeaderName)) {
request.headers.set(UserAgentHeaderName, userAgentValue);
request.headers.set(UserAgentHeaderName, await userAgentValue);
}
return next(request);
},
Expand Down
17 changes: 17 additions & 0 deletions sdk/core/ts-http-runtime/src/util/react-native.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

declare module "react-native" {
export const Platform: {
constants?: {
reactNativeVersion?: {
major: number;
minor: number;
patch: number;
};
};

OS: string;
Version: string;
};
}
4 changes: 2 additions & 2 deletions sdk/core/ts-http-runtime/src/util/userAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export function getUserAgentHeaderName(): string {
/**
* @internal
*/
export function getUserAgentValue(prefix?: string): string {
export async function getUserAgentValue(prefix?: string): Promise<string> {
const runtimeInfo = new Map<string, string>();
runtimeInfo.set("ts-http-runtime", SDK_VERSION);
setPlatformSpecificData(runtimeInfo);
await setPlatformSpecificData(runtimeInfo);
const defaultAgent = getUserAgentString(runtimeInfo);
const userAgentValue = prefix ? `${prefix} ${defaultAgent}` : defaultAgent;
return userAgentValue;
Expand Down
35 changes: 27 additions & 8 deletions sdk/core/ts-http-runtime/src/util/userAgentPlatform-browser.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,39 @@ export function getHeaderName(): string {

interface NavigatorEx extends Navigator {
userAgentData?: {
brands: { brand: string; version: string }[];
mobile: boolean;
platform?: string;
getHighEntropyValues: (hints: string[]) => Promise<{
architecture: string;
bitness: string;
brands: { brand: string; version: string }[];
formFactor: string;
fullVersionList: { brand: string; version: string }[];
mobile: boolean;
model: string;
platform: string;
platformVersion: string;
wow64: boolean;
}>;
};
}

/**
* @internal
*/
export function setPlatformSpecificData(map: Map<string, string>): void {
export async function setPlatformSpecificData(map: Map<string, string>): Promise<void> {
const localNavigator = globalThis.navigator as NavigatorEx;
map.set(
"OS",
(localNavigator?.userAgentData?.platform ?? localNavigator?.platform ?? "unknown").replace(
" ",
"",
),
);
let osPlatform = "unknown";
if (localNavigator.userAgentData) {
const entropyValues = await localNavigator.userAgentData.getHighEntropyValues([
"architecture",
"platformVersion",
]);
osPlatform = `${entropyValues.architecture}-${entropyValues.platform}-${entropyValues.platformVersion}`;
} else if (localNavigator?.platform) {
osPlatform = localNavigator.platform;
}

map.set("OS", osPlatform);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
const { Platform } = await import("react-native");
import { Platform } from "react-native";

/**
* @internal
Expand All @@ -15,7 +13,7 @@ export function getHeaderName(): string {
/**
* @internal
*/
export function setPlatformSpecificData(map: Map<string, string>): void {
export async function setPlatformSpecificData(map: Map<string, string>): Promise<void> {
if (Platform.constants?.reactNativeVersion) {
const { major, minor, patch } = Platform.constants.reactNativeVersion;
map.set("react-native", `${major}.${minor}.${patch}`);
Expand Down
2 changes: 1 addition & 1 deletion sdk/core/ts-http-runtime/src/util/userAgentPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function getHeaderName(): string {
/**
* @internal
*/
export function setPlatformSpecificData(map: Map<string, string>): void {
export async function setPlatformSpecificData(map: Map<string, string>): Promise<void> {
if (process && process.versions) {
const versions = process.versions as ExtendedPlatformVersions;
if (versions.bun) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { describe, it, assert } from "vitest";
import { setPlatformSpecificData } from "../../src/util/userAgentPlatform.js";

describe("userAgentPlatform", () => {
it("should set OS", () => {
it("should set OS", async () => {
const map = new Map<string, string>();

setPlatformSpecificData(map);
await setPlatformSpecificData(map);

assert.ok(map.has("OS"));
});
Expand Down
17 changes: 9 additions & 8 deletions sdk/core/ts-http-runtime/test/node/userAgentPlatform.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,23 @@ describe("userAgentPlatform", () => {
vi.clearAllMocks();
});

it("should handle an empty process.versions", () => {
it("should handle an empty process.versions", async () => {
vi.mocked(process).versions = undefined;
const map = new Map<string, string>();

setPlatformSpecificData(map);
await setPlatformSpecificData(map);

assert.ok(map.has("OS"));
assert.notOk(map.has("Node"));
assert.notOk(map.has("Deno"));
assert.notOk(map.has("Bun"));
});

it("should handle a Node.js process.versions with Bun", () => {
it("should handle a Node.js process.versions with Bun", async () => {
vi.mocked(process).versions = { bun: "1.0.0" };
const map = new Map<string, string>();

setPlatformSpecificData(map);
await setPlatformSpecificData(map);

assert.ok(map.has("OS"));
assert.ok(map.has("Bun"));
Expand All @@ -42,11 +43,11 @@ describe("userAgentPlatform", () => {
assert.notOk(map.has("Deno"));
});

it("should handle a Node.js process.versions with Deno", () => {
it("should handle a Node.js process.versions with Deno", async () => {
vi.mocked(process).versions = { deno: "2.0.0" };
const map = new Map<string, string>();

setPlatformSpecificData(map);
await setPlatformSpecificData(map);

assert.ok(map.has("OS"));
assert.ok(map.has("Deno"));
Expand All @@ -55,11 +56,11 @@ describe("userAgentPlatform", () => {
assert.notOk(map.has("Bun"));
});

it("should handle a Node.js process.versions", () => {
it("should handle a Node.js process.versions", async () => {
vi.mocked(process).versions = { node: "20.0.0" };
const map = new Map<string, string>();

setPlatformSpecificData(map);
await setPlatformSpecificData(map);

assert.ok(map.has("OS"));
assert.ok(map.has("Node"));
Expand Down

0 comments on commit 6c754ae

Please sign in to comment.