Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: upgrade ua-parser-js to track correct device models #3406

Draft
wants to merge 15 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 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
3 changes: 1 addition & 2 deletions packages/hms-video-store/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/hms-video-store/src/analytics/AnalyticsEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,9 @@ export default class AnalyticsEvent implements ISignalParamsProvider<SignalEvent
user_name?: string;
user_data?: string;
};
userAgent: string;
userAgent?: string;
} = {
peer: {},
userAgent: createUserAgent(),
};
timestamp: number;
event_id: string;
Expand All @@ -55,6 +54,7 @@ export default class AnalyticsEvent implements ISignalParamsProvider<SignalEvent
this.timestamp = timestamp || new Date().getTime(); // Timestamp of generating the event
this.event_id = uuid();
this.device_id = getAnalyticsDeviceId();
createUserAgent().then(userAgent => (this.metadata.userAgent = userAgent));
}

toSignalParams() {
Expand Down
13 changes: 8 additions & 5 deletions packages/hms-video-store/src/analytics/HTTPAnalyticsTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,16 @@ class ClientAnalyticsTransport implements IAnalyticsTransportProvider {
},
};
const url = this.env === ENV.PROD ? CLIENT_ANAYLTICS_PROD_ENDPOINT : CLIENT_ANAYLTICS_QA_ENDPOINT;
const headers = new Headers({
'Content-Type': 'application/json',
Authorization: `Bearer ${event.metadata.token}`,
});
if (event.metadata.userAgent) {
headers.set('user_agent_v2', event.metadata.userAgent);
}
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${event.metadata.token}`,
user_agent_v2: event.metadata.userAgent,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this optional?

},
headers,
body: JSON.stringify(requestBody),
})
.then(response => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface HMSSessionFeedback {

export interface HMSSessionInfo {
peer: HMSSessionPeerInfo;
agent: string;
agent?: string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

most likely should not be optional

device_id: string;
cluster: HMSSessionCluster;
timestamp: number;
Expand Down
10 changes: 6 additions & 4 deletions packages/hms-video-store/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ export class HMSSdk implements HMSInterface {
}

this.analyticsTimer.start(TimedEvent.PREVIEW);
this.setUpPreview(config, listener);
await this.setUpPreview(config, listener);

let initSuccessful = false;
let networkTestFinished = false;
Expand Down Expand Up @@ -620,7 +620,7 @@ export class HMSSdk implements HMSInterface {
this.removeDevicesFromConfig(config);
this.store.setConfig(config);
/** set after config since we need config to get env for user agent */
this.store.createAndSetUserAgent(this.frameworkInfo);
await this.store.createAndSetUserAgent(this.frameworkInfo);
HMSAudioContextHandler.resumeContext();
// acquire screen lock to stay awake while in call
const storeConfig = this.store.getConfig();
Expand Down Expand Up @@ -1463,15 +1463,17 @@ export class HMSSdk implements HMSInterface {
* Init store and other managers, setup listeners, create local peer, room
* @param {HMSConfig} config
* @param {HMSPreviewListener} listener
* @returns {Promise<void>} - resolves when store is initialised
*/
private setUpPreview(config: HMSPreviewConfig, listener: HMSPreviewListener) {
private async setUpPreview(config: HMSPreviewConfig, listener: HMSPreviewListener): Promise<void> {
this.listener = listener as unknown as HMSUpdateListener;
this.sdkState.isPreviewCalled = true;
this.sdkState.isPreviewInProgress = true;
const { roomId, userId, role } = decodeJWT(config.authToken);
this.commonSetup(config, roomId, listener);
this.store.setConfig(config);
/** set after config since we need config to get env for user agent */
this.store.createAndSetUserAgent(this.frameworkInfo);
await this.store.createAndSetUserAgent(this.frameworkInfo);
this.createAndAddLocalPeerToStore(config, role, userId, config.asRole);
}

Expand Down
6 changes: 3 additions & 3 deletions packages/hms-video-store/src/sdk/store/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class Store {
private roleDetailsArrived = false;
private env: ENV = ENV.PROD;
private simulcastEnabled = false;
private userAgent: string = createUserAgent(this.env);
private userAgent?: string;
private polls = new Map<string, HMSPoll>();
private whiteboards = new Map<string, HMSWhiteboard>();

Expand Down Expand Up @@ -213,8 +213,8 @@ class Store {
return this.userAgent;
}

createAndSetUserAgent(frameworkInfo?: HMSFrameworkInfo) {
this.userAgent = createUserAgent(this.env, frameworkInfo);
async createAndSetUserAgent(frameworkInfo?: HMSFrameworkInfo) {
this.userAgent = await createUserAgent(this.env, frameworkInfo);
}

setRoom(room: HMSRoom) {
Expand Down
8 changes: 4 additions & 4 deletions packages/hms-video-store/src/signal/init/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { HMSException } from '../../error/HMSException';
import { ENV } from '../../utils/support';
import { createUserAgent } from '../../utils/user-agent';

describe('getUrl', () => {
const userAgent = createUserAgent(ENV.PROD);
describe('getUrl', async () => {
const userAgent = await createUserAgent(ENV.PROD);
const userAgentQueryParam = new URLSearchParams(`user_agent_v2=${userAgent}`).toString();
const peerId = '1234';
it('should return the URL even if unnecesary params are passed to the endpoint', () => {
Expand Down Expand Up @@ -67,7 +67,7 @@ describe('transformInit', () => {
});
});

describe('init API call', () => {
describe('init API call', async () => {
const peerId = '2e26acc7-d2c8-4235-883e-812695ff1e7d';
const correctToken =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhY2Nlc3Nfa2V5IjoiNjEwY2Q5Y2JmMzBlNzczZjQ3NTc3YjBkIiwicm9vbV9pZCI6IjYxOGU5NGY1YWYzMTg4ZGYzM2U2N2Q0NiIsInVzZXJfaWQiOiJiZTM5MzQwZC04ZDgzLTQ5ZjQtOTNhMy00ZjRmMTgwZTVkZWUiLCJyb2xlIjoiaG9zdCIsImp0aSI6IjY0ZTRjMTgzLWZkNTktNGE2OS1hOGY2LWNkNGE5MzBmOTYzZSIsInR5cGUiOiJhcHAiLCJ2ZXJzaW9uIjoyLCJleHAiOjE2NTIyNjUyNzV9.t1Wvwl0tXyMzi386LwfDACvUeWibZYIzSf20DTwjqJU';
Expand All @@ -76,7 +76,7 @@ describe('init API call', () => {
const wrongToken =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhY2Nlc3Nfa2V5IjoiNjEwY2Q5Y2JmMzBlNzczZjQ3NTc3YjBkIiwicm9vbV9pZCI6IjYxOGU5NGY1YWYzMTg4ZGYzM2U2N2Q0NyIsInVzZXJfaWQiOiJiZTM5MzQwZC04ZDgzLTQ5ZjQtOTNhMy00ZjRmMTgwZTVkZWUiLCJyb2xlIjoiaG9zdCIsImp0aSI6IjY0ZTRjMTgzLWZkNTktNGE2OS1hOGY2LWNkNGE5MzBmOTYzZSIsInR5cGUiOiJhcHAiLCJ2ZXJzaW9uIjoyLCJleHAiOjE2NTIyNjUyNzV9.tX4BZllTjOuA5L3bgItoDYKQa6J3d-L2cayvQiEntHY';

const userAgent = createUserAgent(ENV.PROD);
const userAgent = await createUserAgent(ENV.PROD);

const mockResponse = (init: RequestInit | undefined): Promise<Response> => {
const headers = init?.headers as Record<string, string>;
Expand Down
12 changes: 10 additions & 2 deletions packages/hms-video-store/src/transport/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1092,11 +1092,15 @@ export default class HMSTransport {
HMSLogger.d(TAG, 'connect: started ⏰');
const connectRequestedAt = new Date();
try {
const userAgent = this.store.getUserAgent();
if (!userAgent) {
throw ErrorFactory.GenericErrors.PeerMetadataMissing(HMSAction.INIT, 'User Agent not available');
}
this.analyticsTimer.start(TimedEvent.INIT);
this.initConfig = await InitService.fetchInitConfig({
token,
peerId,
userAgent: this.store.getUserAgent(),
userAgent,
initEndpoint,
iceServers,
});
Expand Down Expand Up @@ -1152,12 +1156,16 @@ export default class HMSTransport {
if (!this.initConfig) {
throw ErrorFactory.APIErrors.InitConfigNotAvailable(HMSAction.INIT, 'Init Config not found');
}
const userAgent = this.store.getUserAgent();
if (!userAgent) {
throw ErrorFactory.GenericErrors.PeerMetadataMissing(HMSAction.INIT, 'User Agent not available');
}

HMSLogger.d(TAG, '⏳ internal connect: connecting to ws endpoint', this.initConfig.endpoint);
const url = new URL(this.initConfig.endpoint);
url.searchParams.set('peer', peerId);
url.searchParams.set('token', token);
url.searchParams.set('user_agent_v2', this.store.getUserAgent());
url.searchParams.set('user_agent_v2', userAgent);
url.searchParams.set('protocol_version', PROTOCOL_VERSION);
url.searchParams.set('protocol_spec', PROTOCOL_SPEC);

Expand Down
3 changes: 1 addition & 2 deletions packages/hms-video-store/src/utils/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export const isPageHidden = () => typeof document !== 'undefined' && document.hi

export const isIOS = () => parsedUserAgent.getOS().name?.toLowerCase() === 'ios';

export const isFirefox = parsedUserAgent.getBrowser()?.name?.toLowerCase().includes('firefox');
// safari for mac and mobile safari for iOS
export const isSafari = parsedUserAgent.getBrowser()?.name?.toLowerCase().includes('safari');

export const isFirefox = parsedUserAgent.getBrowser()?.name?.toLowerCase() === 'firefox';
165 changes: 152 additions & 13 deletions packages/hms-video-store/src/utils/user-agent.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type UAParser from 'ua-parser-js';
import { ENV, isNode, parsedUserAgent } from './support';
import { isPresent } from './validations';
import { DomainCategory } from '../analytics/AnalyticsEventDomains';
Expand All @@ -20,7 +21,12 @@ type UserAgent = {
framework_sdk_version?: HMSFrameworkInfo['sdkVersion'];
};

export function createUserAgent(sdkEnv: ENV = ENV.PROD, frameworkInfo?: HMSFrameworkInfo): string {
/**
* Create UserAgent string
* @param sdkEnv - SDK environment
* @param frameworkInfo - Framework information
*/
export async function createUserAgent(sdkEnv: ENV = ENV.PROD, frameworkInfo?: HMSFrameworkInfo) {
const sdk = 'web';
const env = domainCategory !== DomainCategory.LOCAL && sdkEnv === ENV.PROD ? 'prod' : 'debug';

Expand All @@ -39,21 +45,25 @@ export function createUserAgent(sdkEnv: ENV = ENV.PROD, frameworkInfo?: HMSFrame
});
}

const parsedOs = parsedUserAgent.getOS();
const parsedDevice = parsedUserAgent.getDevice();
const parsedBrowser = parsedUserAgent.getBrowser();
console.error(
'client hints',
await parsedUserAgent.getOS().withClientHints(),
await parsedUserAgent.getDevice().withClientHints(),
await parsedUserAgent.getBrowser().withClientHints(),
);

const os = replaceSpaces(`web_${parsedOs.name}`);
const os_version = parsedOs.version || '';
console.error(
'feature check: ',
await parsedUserAgent.getOS().withFeatureCheck(),
await parsedUserAgent.getDevice().withFeatureCheck(),
await parsedUserAgent.getBrowser().withFeatureCheck(),
);

const browser = replaceSpaces(`${parsedBrowser.name}_${parsedBrowser.version}`);
let device_model = browser;
if (parsedDevice.type) {
const deviceVendor = replaceSpaces(`${parsedDevice.vendor}_${parsedDevice.type}`);
device_model = `${deviceVendor}/${browser}`;
}
const { os, version: os_version } = await getOS();

const device_model = await getDevice();

return convertObjectToString({
const ua = convertObjectToString({
os,
os_version,
sdk,
Expand All @@ -66,6 +76,135 @@ export function createUserAgent(sdkEnv: ENV = ENV.PROD, frameworkInfo?: HMSFrame
framework_version: frameworkInfo?.version,
framework_sdk_version: frameworkInfo?.sdkVersion,
});
console.error('UserAgent:', ua);
return ua;
}

/**
* Get OS name and version
* @internal
* @returns {Promise<{ os: string; version: string }>} OS name and version
*/
async function getOS(): Promise<{ os: string; version: string }> {
const { name, version } = await getOSFromUserAgent();
return getFormattedOS({ name, version });
}

/**
* Get OS name and version initially from UserAgent with ClientHints and then with FeatureCheck
* @internal
* @returns {Promise<UAParser.IOS>} OS name and version
*/
async function getOSFromUserAgent(): Promise<UAParser.IOS> {
const os = await parsedUserAgent.getOS().withClientHints();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might fail in older devices/browsers no? should we add a try catch and do feature check there as well?

if (!os.name || os.name.length === 0 || !os.version || os.version.length === 0) {
return parsedUserAgent.getOS().withFeatureCheck();
}
return os;
}

/**
* Get formatted OS name and version
* @internal
* @param { name, version } - OS name and version
* @returns { { os: string; version: string } } Formatted OS name and version
*/
function getFormattedOS({ name, version }: { name?: string; version?: string }): { os: string; version: string } {
return {
os: replaceSpaces(`web_${name}`),
version: version || '',
};
}

/**
* Get Browser name and version
* @internal
* @returns {Promise<string | undefined>} Browser name and version string
*/
async function getBrowser(): Promise<string | undefined> {
const { name, version } = await getBrowserFromUserAgent();
return getFormattedBrowser({ name, version });
}

/**
* Get Browser name and version initially from UserAgent with ClientHints and then with FeatureCheck
* @internal
* @returns {Promise<UAParser.IBrowser>} Browser name and version
*/
async function getBrowserFromUserAgent(): Promise<UAParser.IBrowser> {
const browser = await parsedUserAgent.getBrowser().withClientHints();
if (!browser.name || browser.name.length === 0 || !browser.version || browser.version.length === 0) {
return parsedUserAgent.getBrowser().withFeatureCheck();
}
return browser;
}

/**
* Get formatted Browser name and version
* @param {name, version} - Browser name and version
* @returns {string | undefined} Formatted Browser name and version string
*/
function getFormattedBrowser({ name, version }: { name?: string; version?: string }): string | undefined {
return name ? `${replaceSpaces(name)}_${version}` : version;
}

/**
* Get Device name string
* @internal
* @returns {Promise<string | undefined>} Device name string
*/
async function getDevice(): Promise<string | undefined> {
const device = getFormattedDevice(await getDeviceFromUserAgent());
const browser = await getBrowser();
return device ? `${device}/${browser}` : browser;
}

/**
* Get Device name string initially from UserAgent with FeatureCheck and then with ClientHints
* @internal
* @returns {Promise<UAParser.IDevice>} Device name string
*/
async function getDeviceFromUserAgent(): Promise<UAParser.IDevice> {
const device = await parsedUserAgent.getDevice().withFeatureCheck();
if (!device.vendor || device.vendor.length === 0 || !device.type || device.type.length === 0) {
return parsedUserAgent.getDevice().withClientHints();
}
return device;
}

/**
* Get formatted Device name string
* @param {vendor, type, model} - Device vendor, type and model
* @returns {string | undefined} Formatted Device name string
*/
function getFormattedDevice({
vendor,
type,
model,
}: {
vendor?: string;
type?: string;
model?: string;
}): string | undefined {
let device = undefined;
if (vendor) {
device = vendor;
}
if (type) {
device = getDeviceType(device, type);
}
if (model) {
device = getDeviceModel(device, model);
}
return device ? replaceSpaces(device) : undefined;
}

function getDeviceType(device?: string, type?: string) {
return device && device.length > 0 ? `${device}_${type}` : type;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

length check probably not needed

}

function getDeviceModel(device?: string, model?: string) {
return device && device.length > 0 ? `${device}_${model}` : model;
}

function replaceSpaces(s: string) {
Expand Down
Loading
Loading