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..c3edf503 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'; @@ -433,6 +434,7 @@ export const SplunkRum: SplunkOtelWebType = { // Splunk specific attributes 'splunk.rumVersion': VERSION, 'splunk.scriptInstance': instanceId, + 'browser.instance.id': BrowserInstanceService.id, 'app': applicationName, }; diff --git a/packages/web/src/services/BrowserInstanceService.ts b/packages/web/src/services/BrowserInstanceService.ts new file mode 100644 index 00000000..0329b6c0 --- /dev/null +++ b/packages/web/src/services/BrowserInstanceService.ts @@ -0,0 +1,53 @@ +/* +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 { + static _id: string | undefined = undefined; + + static get id(): string { + if(this._id) { + 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) { + browserInstanceId = generateId(64); + safelySetSessionStorage(BROWSER_INSTANCE_ID_KEY, browserInstanceId); + } + + this._id = browserInstanceId; + + 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..53914e73 --- /dev/null +++ b/packages/web/src/utils/storage.ts @@ -0,0 +1,37 @@ +/* +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 => { + let value = null; + try { + value = window.sessionStorage.getItem(key); + } catch { + // sessionStorage not accessible probably user is in incognito-mode + // or set "Block third-party cookies" option in browser settings + } + return value; +}; + +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; + } +};