diff --git a/packages/hms-video-store/package.json b/packages/hms-video-store/package.json index 1d6abfe719..322f197061 100644 --- a/packages/hms-video-store/package.json +++ b/packages/hms-video-store/package.json @@ -51,7 +51,7 @@ "lodash.isequal": "^4.5.0", "reselect": "4.0.0", "sdp-transform": "^2.14.1", - "ua-parser-js": "^1.0.1", + "ua-parser-js": "^2.0.0", "uuid": "^8.3.2", "webrtc-adapter": "^8.0.0", "zustand": "3.5.7" @@ -60,7 +60,6 @@ "@types/dom-screen-wake-lock": "^1.0.1", "@types/lodash.isequal": "^4.5.8", "@types/sdp-transform": "^2.4.4", - "@types/ua-parser-js": "^0.7.36", "@types/uuid": "^8.3.0", "jest-canvas-mock": "^2.3.1", "jsdom-worker": "^0.3.0", diff --git a/packages/hms-video-store/src/analytics/AnalyticsEvent.ts b/packages/hms-video-store/src/analytics/AnalyticsEvent.ts index c31174b846..ddf1aa722a 100644 --- a/packages/hms-video-store/src/analytics/AnalyticsEvent.ts +++ b/packages/hms-video-store/src/analytics/AnalyticsEvent.ts @@ -38,10 +38,9 @@ export default class AnalyticsEvent implements ISignalParamsProvider (this.metadata.userAgent = userAgent)); } toSignalParams() { diff --git a/packages/hms-video-store/src/analytics/HTTPAnalyticsTransport.ts b/packages/hms-video-store/src/analytics/HTTPAnalyticsTransport.ts index e53d020775..5f2059b0a3 100644 --- a/packages/hms-video-store/src/analytics/HTTPAnalyticsTransport.ts +++ b/packages/hms-video-store/src/analytics/HTTPAnalyticsTransport.ts @@ -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, - }, + headers, body: JSON.stringify(requestBody), }) .then(response => { diff --git a/packages/hms-video-store/src/end-call-feedback/interface.ts b/packages/hms-video-store/src/end-call-feedback/interface.ts index f2a5ae398d..69b8efbdec 100644 --- a/packages/hms-video-store/src/end-call-feedback/interface.ts +++ b/packages/hms-video-store/src/end-call-feedback/interface.ts @@ -15,7 +15,7 @@ export interface HMSSessionFeedback { export interface HMSSessionInfo { peer: HMSSessionPeerInfo; - agent: string; + agent?: string; device_id: string; cluster: HMSSessionCluster; timestamp: number; diff --git a/packages/hms-video-store/src/sdk/index.ts b/packages/hms-video-store/src/sdk/index.ts index eaa7c2f82f..981cb14aaa 100644 --- a/packages/hms-video-store/src/sdk/index.ts +++ b/packages/hms-video-store/src/sdk/index.ts @@ -451,7 +451,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; @@ -621,7 +621,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(); @@ -1470,15 +1470,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} - resolves when store is initialised */ - private setUpPreview(config: HMSPreviewConfig, listener: HMSPreviewListener) { + private async setUpPreview(config: HMSPreviewConfig, listener: HMSPreviewListener): Promise { + 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); } diff --git a/packages/hms-video-store/src/sdk/store/Store.ts b/packages/hms-video-store/src/sdk/store/Store.ts index c08e55bbe8..0ab18458fd 100644 --- a/packages/hms-video-store/src/sdk/store/Store.ts +++ b/packages/hms-video-store/src/sdk/store/Store.ts @@ -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(); private whiteboards = new Map(); @@ -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) { diff --git a/packages/hms-video-store/src/signal/init/init.test.ts b/packages/hms-video-store/src/signal/init/init.test.ts index a987765d47..359c4ae666 100644 --- a/packages/hms-video-store/src/signal/init/init.test.ts +++ b/packages/hms-video-store/src/signal/init/init.test.ts @@ -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', () => { @@ -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'; @@ -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 => { const headers = init?.headers as Record; diff --git a/packages/hms-video-store/src/transport/index.ts b/packages/hms-video-store/src/transport/index.ts index 6615b030cc..66e6eb6a28 100644 --- a/packages/hms-video-store/src/transport/index.ts +++ b/packages/hms-video-store/src/transport/index.ts @@ -1093,11 +1093,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, }); @@ -1153,12 +1157,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); diff --git a/packages/hms-video-store/src/utils/support.ts b/packages/hms-video-store/src/utils/support.ts index 8e1ef456c7..ea53847475 100644 --- a/packages/hms-video-store/src/utils/support.ts +++ b/packages/hms-video-store/src/utils/support.ts @@ -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'; diff --git a/packages/hms-video-store/src/utils/user-agent.ts b/packages/hms-video-store/src/utils/user-agent.ts index 335d6c4ffc..f3937d3977 100644 --- a/packages/hms-video-store/src/utils/user-agent.ts +++ b/packages/hms-video-store/src/utils/user-agent.ts @@ -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'; @@ -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'; @@ -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, @@ -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} OS name and version + */ +async function getOSFromUserAgent(): Promise { + const os = await parsedUserAgent.getOS().withClientHints(); + 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} Browser name and version string + */ +async function getBrowser(): Promise { + 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} Browser name and version + */ +async function getBrowserFromUserAgent(): Promise { + 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} Device name string + */ +async function getDevice(): Promise { + 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} Device name string + */ +async function getDeviceFromUserAgent(): Promise { + 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; +} + +function getDeviceModel(device?: string, model?: string) { + return device && device.length > 0 ? `${device}_${model}` : model; } function replaceSpaces(s: string) { diff --git a/packages/roomkit-react/src/Prebuilt/common/constants.ts b/packages/roomkit-react/src/Prebuilt/common/constants.ts index 73ebf7fff3..5dc26cff3e 100644 --- a/packages/roomkit-react/src/Prebuilt/common/constants.ts +++ b/packages/roomkit-react/src/Prebuilt/common/constants.ts @@ -104,11 +104,12 @@ export const HLS_TIMED_METADATA_DOC_URL = export const REMOTE_STOP_SCREENSHARE_TYPE = 'REMOTE_STOP_SCREENSHARE'; -export const isChrome = parsedUserAgent.getBrowser()?.name?.toLowerCase() === 'chrome'; -export const isFirefox = parsedUserAgent.getBrowser()?.name?.toLowerCase() === 'firefox'; -export const isSafari = parsedUserAgent.getBrowser()?.name?.toLowerCase() === 'safari'; +// mweb could have browser name as "Mobile Chrome" or "Mobile Firefox" +export const isChrome = parsedUserAgent.getBrowser()?.name?.toLowerCase().includes('chrome'); +export const isFirefox = parsedUserAgent.getBrowser()?.name?.toLowerCase().includes('firefox'); +export const isSafari = parsedUserAgent.getBrowser()?.name?.toLowerCase().includes('safari'); export const isIOS = parsedUserAgent.getOS()?.name?.toLowerCase() === 'ios'; -export const isMacOS = parsedUserAgent.getOS()?.name?.toLowerCase() === 'mac os'; +export const isMacOS = parsedUserAgent.getOS()?.name?.toLowerCase() === 'macos'; export const isAndroid = parsedUserAgent.getOS()?.name?.toLowerCase() === 'android'; export const isIPadOS = false; // typeof navigator !== "undefined" && diff --git a/yarn.lock b/yarn.lock index 1dbd2dbfd4..fe198acbf6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6305,11 +6305,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/ua-parser-js@^0.7.36": - version "0.7.36" - resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190" - integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ== - "@types/unist@^2.0.0": version "2.0.7" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.7.tgz#5b06ad6894b236a1d2bd6b2f07850ca5c59cf4d6" @@ -8973,6 +8968,11 @@ destroy@1.2.0: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== +detect-europe-js@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/detect-europe-js/-/detect-europe-js-0.1.2.tgz#aa76642e05dae786efc2e01a23d4792cd24c7b88" + integrity sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow== + detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" @@ -12058,6 +12058,11 @@ is-ssh@^1.4.0: dependencies: protocols "^2.0.1" +is-standalone-pwa@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz#7a1b0459471a95378aa0764d5dc0a9cec95f2871" + integrity sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g== + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -17671,10 +17676,19 @@ typescript@~4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== -ua-parser-js@^1.0.1: - version "1.0.35" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.35.tgz#c4ef44343bc3db0a3cbefdf21822f1b1fc1ab011" - integrity sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA== +ua-is-frozen@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz#bfbc5f06336e379590e36beca444188c7dc3a7f3" + integrity sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw== + +ua-parser-js@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-2.0.0.tgz#fae88e352510198bd29a6dd41624c7cd0d2c7ade" + integrity sha512-SASgD4RlB7+SCMmlVNqrhPw0f/2pGawWBzJ2+LwGTD0GgNnrKGzPJDiraGHJDwW9Zm5DH2lTmUpqDpbZjJY4+Q== + dependencies: + detect-europe-js "^0.1.2" + is-standalone-pwa "^0.1.1" + ua-is-frozen "^0.1.2" uglify-js@^3.1.4: version "3.17.4"