From 2a58d985916223e3bf30cb8560018623814d8041 Mon Sep 17 00:00:00 2001 From: Jozef Harag Date: Wed, 6 Nov 2024 10:32:06 +0100 Subject: [PATCH] feat: attach `browser.instance.id` and `browser.instance.visibility_state` to spans (#878) --- .../tests/cookies/cookies.spec.js | 7 ++- .../web/src/SplunkSpanAttributesProcessor.ts | 1 + packages/web/src/index.ts | 5 ++ .../src/services/BrowserInstanceService.ts | 60 +++++++++++++++++++ packages/web/src/utils/storage.ts | 36 +++++++++++ 5 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 packages/web/src/services/BrowserInstanceService.ts create mode 100644 packages/web/src/utils/storage.ts diff --git a/packages/web/integration-tests/tests/cookies/cookies.spec.js b/packages/web/integration-tests/tests/cookies/cookies.spec.js index b9906ad5..10a70d9b 100644 --- a/packages/web/integration-tests/tests/cookies/cookies.spec.js +++ b/packages/web/integration-tests/tests/cookies/cookies.spec.js @@ -26,15 +26,18 @@ module.exports = { // This should create two streams of documentLoad sequences, all with the same sessionId but having // two scriptInstances (one from parent, one from iframe) const parent = await browser.globals.findSpan(span => span.name === 'documentFetch' && span.tags['location.href'].includes('cookies.ejs')); - await browser.assert.ok(parent.tags['splunk.rumSessionId']); + await browser.assert.ok(parent.tags['browser.instance.id']); + await browser.assert.notEqual(parent.tags['splunk.scriptInstance'], parent.tags['splunk.rumSessionId']); const iframe = await browser.globals.findSpan(span => span.name === 'documentFetch' && span.tags['location.href'].includes('iframe.ejs')); await browser.assert.ok(iframe.tags['splunk.rumSessionId']); await browser.assert.notEqual(iframe.tags['splunk.scriptInstance'], iframe.tags['splunk.rumSessionId']); - // same session id + // same session id & instanceId await browser.assert.equal(parent.tags['splunk.rumSessionId'], iframe.tags['splunk.rumSessionId']); + await browser.assert.equal(parent.tags['browser.instance.id'], iframe.tags['browser.instance.id']); + // but different scriptInstance await browser.assert.notEqual(parent.tags['splunk.scriptInstance'], iframe.tags['splunk.scriptInstance']); diff --git a/packages/web/src/SplunkSpanAttributesProcessor.ts b/packages/web/src/SplunkSpanAttributesProcessor.ts index f254f24a..95bf76d3 100644 --- a/packages/web/src/SplunkSpanAttributesProcessor.ts +++ b/packages/web/src/SplunkSpanAttributesProcessor.ts @@ -47,6 +47,7 @@ export class SplunkSpanAttributesProcessor implements SpanProcessor { span.setAttribute('location.href', location.href); span.setAttributes(this._globalAttributes); span.setAttribute('splunk.rumSessionId', getRumSessionId()); + span.setAttribute('browser.instance.visibility_state', document.visibilityState); } onEnd(): void { diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts index 0eeca9d4..1a2e7c71 100644 --- a/packages/web/src/index.ts +++ b/packages/web/src/index.ts @@ -70,6 +70,7 @@ import { SessionBasedSampler } from './SessionBasedSampler'; import { SocketIoClientInstrumentationConfig, SplunkSocketIoClientInstrumentation } from './SplunkSocketIoClientInstrumentation'; import { SplunkOTLPTraceExporter } from './exporters/otlp'; import { registerGlobal, unregisterGlobal } from './global-utils'; +import { BrowserInstanceService } from './services/BrowserInstanceService'; export { SplunkExporterConfig } from './exporters/common'; export { SplunkZipkinExporter } from './exporters/zipkin'; @@ -436,6 +437,10 @@ export const SplunkRum: SplunkOtelWebType = { 'app': applicationName, }; + if(BrowserInstanceService.id) { + resourceAttrs['browser.instance.id'] = BrowserInstanceService.id; + } + const syntheticsRunId = getSyntheticsRunId(); if (syntheticsRunId) { resourceAttrs[SYNTHETICS_RUN_ID_ATTRIBUTE] = syntheticsRunId; diff --git a/packages/web/src/services/BrowserInstanceService.ts b/packages/web/src/services/BrowserInstanceService.ts new file mode 100644 index 00000000..e8571e8b --- /dev/null +++ b/packages/web/src/services/BrowserInstanceService.ts @@ -0,0 +1,60 @@ +/* +Copyright 2024 Splunk Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { safelyGetSessionStorage, safelySetSessionStorage } from '../utils/storage'; +import { generateId } from '../utils'; + +const BROWSER_INSTANCE_ID_KEY = 'browser_instance_id'; + +/** + * BrowserInstanceService is responsible for generating and storing a unique ID for the tab. + * THe ID is stored in the session storage, so stays the same even after page reload + * as long as the tab stays on the same domain. + * This ID will be the same for all frames/context in the tab as long as they are on the same domain. + * Currently, this is simplified version which has this limitation: + * - It does not cover the case when tab is duplicated - + * browsers copy storage when duplicating tabs so the ID will be the same + * To cover this case we need to implement a communication between tabs which requires asynchronous initialization. + * This is not implemented yet as requires bigger refactoring. + */ +export class BrowserInstanceService { + // `undefined` represents the case when the storage is inaccessible. + static _id: string | undefined | null = null; + + static get id(): string | undefined { + if(this._id !== null) { + return this._id; + } + + + // Check if the ID is already stored in the session storage. It might be generated by another frame/context. + let browserInstanceId = safelyGetSessionStorage(BROWSER_INSTANCE_ID_KEY); + if(browserInstanceId) { + this._id = browserInstanceId; + } else if(browserInstanceId === null) { + // Storage is accessible but the ID is not stored yet. + browserInstanceId = generateId(64); + this._id = browserInstanceId; + safelySetSessionStorage(BROWSER_INSTANCE_ID_KEY, browserInstanceId); + } else { + // Storage is not accessible. + this._id = undefined; + } + + + return this._id; + } +} \ No newline at end of file diff --git a/packages/web/src/utils/storage.ts b/packages/web/src/utils/storage.ts new file mode 100644 index 00000000..11abbde1 --- /dev/null +++ b/packages/web/src/utils/storage.ts @@ -0,0 +1,36 @@ +/* +Copyright 2024 Splunk Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export const safelyGetSessionStorage = (key: string): string | null | undefined => { + try { + return window.sessionStorage.getItem(key); + } catch { + return undefined; + // sessionStorage not accessible probably user is in incognito-mode + // or set "Block third-party cookies" option in browser settings + } +}; + +export const safelySetSessionStorage = (key: string, value: string): boolean => { + try { + window.sessionStorage.setItem(key, value); + return true; + } catch { + // sessionStorage not accessible probably user is in incognito-mode + // or set "Block third-party cookies" option in browser settings + return false; + } +};