-
Notifications
You must be signed in to change notification settings - Fork 69
chore(analytics-browser): add platform and web_environment tags to diagnostics #1914
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
Merged
Merged
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
6b7a8a4
feat(analytics-browser): add platform tag to diagnostics
Mercy811 5659eb6
feat(analytics-core): add web environment detection helpers
Mercy811 48500e7
feat(analytics-browser): add web_environment tag to diagnostics
Mercy811 806320b
refactor(analytics-browser): move web environment detection out of core
Mercy811 f2bd14a
refactor(analytics-browser): rename getWebEnvironment to getRuntimeEn…
Mercy811 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { getGlobalScope } from '@amplitude/analytics-core'; | ||
|
|
||
| export type WebEnvironment = | ||
| | 'browser' | ||
| | 'web_worker' | ||
| | 'service_worker' | ||
| | 'chrome_extension' | ||
| | 'chrome_extension_service_worker' | ||
| | 'node' | ||
| | 'unknown'; | ||
|
|
||
| type Scope = NonNullable<ReturnType<typeof getGlobalScope>>; | ||
|
|
||
| // All predicates take the already-resolved global scope so the whole classification | ||
| // reads one consistent object. | ||
|
|
||
| // Same check as core's isChromeExtension, on the resolved scope: extension pages, | ||
| // content scripts, and MV3 background workers all expose chrome.runtime.id. | ||
| const isChromeExtension = (scope: Scope): boolean => { | ||
| return typeof (scope as { chrome?: { runtime?: { id?: unknown } } }).chrome?.runtime?.id === 'string'; | ||
| }; | ||
|
|
||
| // A service worker global, including an MV3 extension background worker. | ||
| // ServiceWorkerGlobalScope inherits from WorkerGlobalScope, so isWebWorker also matches | ||
| // here — getWebEnvironment checks this one first. | ||
| const isServiceWorker = (scope: Scope): boolean => { | ||
| const ctor = (scope as { ServiceWorkerGlobalScope?: new () => unknown }).ServiceWorkerGlobalScope; | ||
| return typeof ctor === 'function' && scope instanceof ctor; | ||
| }; | ||
|
|
||
| // Dedicated / shared / service worker. `WorkerGlobalScope` is only exposed on worker | ||
| // globals, so its presence is already worker-specific; the instanceof check additionally | ||
| // rules out non-browser runtimes that expose web constructors on a server global. | ||
| const isWebWorker = (scope: Scope): boolean => { | ||
| const ctor = (scope as { WorkerGlobalScope?: new () => unknown }).WorkerGlobalScope; | ||
| return typeof ctor === 'function' && scope instanceof ctor; | ||
| }; | ||
|
|
||
| // Main-thread only: workers have no document. | ||
| const isBrowser = (scope: Scope): boolean => { | ||
| return typeof (scope as { document?: unknown }).document !== 'undefined'; | ||
| }; | ||
|
|
||
| // Checks process.versions.node rather than bare process: bundlers commonly inject a | ||
| // process.env shim into browser builds, but not versions.node. | ||
| const isNode = (scope: Scope): boolean => { | ||
| return typeof (scope as { process?: { versions?: { node?: unknown } } }).process?.versions?.node === 'string'; | ||
| }; | ||
|
|
||
| /** | ||
| * Classifies the JS runtime for diagnostics. The check order is load-bearing: | ||
| * - service worker before web worker: ServiceWorkerGlobalScope inherits from WorkerGlobalScope | ||
| * - browser after the extension split: extension pages and content scripts have a `document` | ||
| * - browser before node: jsdom exposes both `document` and `process.versions.node` | ||
| */ | ||
| export function getWebEnvironment(): WebEnvironment { | ||
| const scope = getGlobalScope(); | ||
| if (!scope) { | ||
| return 'unknown'; | ||
| } | ||
| const isExtension = isChromeExtension(scope); | ||
| if (isServiceWorker(scope)) { | ||
| return isExtension ? 'chrome_extension_service_worker' : 'service_worker'; | ||
| } | ||
| if (isWebWorker(scope)) { | ||
| return 'web_worker'; | ||
| } | ||
| if (isBrowser(scope)) { | ||
| return isExtension ? 'chrome_extension' : 'browser'; | ||
| } | ||
| if (isNode(scope)) { | ||
| return 'node'; | ||
| } | ||
| return 'unknown'; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import * as core from '@amplitude/analytics-core'; | ||
| import { getWebEnvironment } from '../../src/utils/environment'; | ||
|
|
||
| // Stands in for a worker global: an instance of the constructor it also exposes. | ||
| class FakeWorkerGlobalScope { | ||
| WorkerGlobalScope = FakeWorkerGlobalScope; | ||
| } | ||
|
|
||
| // Mirrors the real inheritance: ServiceWorkerGlobalScope extends WorkerGlobalScope. | ||
| class FakeServiceWorkerGlobalScope extends FakeWorkerGlobalScope { | ||
| ServiceWorkerGlobalScope = FakeServiceWorkerGlobalScope; | ||
| } | ||
|
|
||
| const asScope = (scope: unknown) => scope as typeof globalThis; | ||
| const workerGlobalScope = () => asScope(new FakeWorkerGlobalScope()); | ||
| const serviceWorkerGlobalScope = () => asScope(new FakeServiceWorkerGlobalScope()); | ||
|
|
||
| describe('getWebEnvironment', () => { | ||
| let getGlobalScopeSpy: jest.SpyInstance; | ||
|
|
||
| beforeEach(() => { | ||
| getGlobalScopeSpy = jest.spyOn(core, 'getGlobalScope'); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| getGlobalScopeSpy.mockRestore(); | ||
| }); | ||
|
|
||
| test('returns unknown when globalScope is undefined', () => { | ||
| getGlobalScopeSpy.mockReturnValue(undefined); | ||
| expect(getWebEnvironment()).toBe('unknown'); | ||
| }); | ||
|
|
||
| test('returns service_worker in a service worker scope, not web_worker', () => { | ||
| // The fake inherits from the worker scope like the real one, so this also | ||
| // covers the service-worker-before-web-worker precedence. | ||
| getGlobalScopeSpy.mockReturnValue(serviceWorkerGlobalScope()); | ||
| expect(getWebEnvironment()).toBe('service_worker'); | ||
| }); | ||
|
|
||
| test('returns chrome_extension_service_worker in an MV3 extension background', () => { | ||
| const scope = Object.assign(serviceWorkerGlobalScope(), { chrome: { runtime: { id: 'ext-abc' } } }); | ||
| getGlobalScopeSpy.mockReturnValue(scope); | ||
| expect(getWebEnvironment()).toBe('chrome_extension_service_worker'); | ||
| }); | ||
|
|
||
| test('returns web_worker in a dedicated or shared worker scope', () => { | ||
| getGlobalScopeSpy.mockReturnValue(workerGlobalScope()); | ||
| expect(getWebEnvironment()).toBe('web_worker'); | ||
| }); | ||
|
|
||
| test('returns browser when a document is present', () => { | ||
| getGlobalScopeSpy.mockReturnValue(asScope({ document: {} })); | ||
| expect(getWebEnvironment()).toBe('browser'); | ||
| }); | ||
|
|
||
| test('returns chrome_extension for extension pages and content scripts', () => { | ||
| getGlobalScopeSpy.mockReturnValue(asScope({ document: {}, chrome: { runtime: { id: 'ext-abc' } } })); | ||
| expect(getWebEnvironment()).toBe('chrome_extension'); | ||
| }); | ||
|
|
||
| test('returns browser when chrome.runtime.id is not a string', () => { | ||
| getGlobalScopeSpy.mockReturnValue(asScope({ document: {}, chrome: { runtime: { id: 1 } } })); | ||
| expect(getWebEnvironment()).toBe('browser'); | ||
| }); | ||
|
|
||
| test('ignores worker constructors the scope is not an instance of', () => { | ||
| // e.g. a server runtime that exposes web constructors on its global. | ||
| getGlobalScopeSpy.mockReturnValue( | ||
| asScope({ | ||
| ServiceWorkerGlobalScope: FakeServiceWorkerGlobalScope, | ||
| WorkerGlobalScope: FakeWorkerGlobalScope, | ||
| document: {}, | ||
| }), | ||
| ); | ||
| expect(getWebEnvironment()).toBe('browser'); | ||
| }); | ||
|
|
||
| test('returns node when process.versions.node is present without a document', () => { | ||
| getGlobalScopeSpy.mockReturnValue(asScope({ process: { versions: { node: '20.0.0' } } })); | ||
| expect(getWebEnvironment()).toBe('node'); | ||
| }); | ||
|
|
||
| test('returns unknown when nothing matches', () => { | ||
| getGlobalScopeSpy.mockReturnValue(asScope({})); | ||
| expect(getWebEnvironment()).toBe('unknown'); | ||
| }); | ||
|
|
||
| test('returns unknown for shim-only chrome and process objects', () => { | ||
| // chrome without runtime; a bundler-injected process.env shim without versions.node. | ||
| getGlobalScopeSpy.mockReturnValue(asScope({ chrome: {}, process: { env: {} } })); | ||
| expect(getWebEnvironment()).toBe('unknown'); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.