diff --git a/.size-limit.cjs b/.size-limit.cjs index d5fbe642..d418d11d 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -24,7 +24,7 @@ module.exports = [ { name: 'artifacts/splunk-otel-web.js', - limit: '42 kB', + limit: '43 kB', path: './packages/web/dist/artifacts/splunk-otel-web.js', }, diff --git a/packages/integration-tests/src/pages/record-page.ts b/packages/integration-tests/src/pages/record-page.ts index f1ab35ae..0923f194 100644 --- a/packages/integration-tests/src/pages/record-page.ts +++ b/packages/integration-tests/src/pages/record-page.ts @@ -45,8 +45,8 @@ export class RecordPage { async flushData() { await this.page.evaluate(() => { - if (window.SplunkRum) { - window.SplunkRum._processor.forceFlush() + if ((window as any).SplunkRum) { + ;(window as any).SplunkRum._processor.forceFlush() } }) } diff --git a/packages/integration-tests/src/server/render-agent.ts b/packages/integration-tests/src/server/render-agent.ts index 971afe0e..6f3a829e 100644 --- a/packages/integration-tests/src/server/render-agent.ts +++ b/packages/integration-tests/src/server/render-agent.ts @@ -20,14 +20,16 @@ export const RENDER_AGENT_TEMPLATE = ` + + + diff --git a/packages/integration-tests/src/tests/extend-activity/extend-activity.spec.ts b/packages/integration-tests/src/tests/extend-activity/extend-activity.spec.ts new file mode 100644 index 00000000..1b397683 --- /dev/null +++ b/packages/integration-tests/src/tests/extend-activity/extend-activity.spec.ts @@ -0,0 +1,77 @@ +/** + * + * Copyright 2020-2025 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 + * + * https://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 { expect } from '@playwright/test' +import { test } from '../../utils/test' + +test.describe('extend-activity', () => { + test('only user activity extends session', async ({ recordPage }) => { + await recordPage.goTo('/extend-activity/some.ejs') + + const sessionCookie1 = await recordPage.getCookie('_splunk_rum_sid') + + expect(sessionCookie1).toBeTruthy() + + const sessionCookie1Parsed = JSON.parse(decodeURIComponent(sessionCookie1.value)) + + await recordPage.waitForTimeout(1500) + + await recordPage.evaluate(() => { + ;(window as any).SplunkRum.provider.getTracer('guard').startSpan('guard-span').end() + }) + + await recordPage.waitForTimeout(1500) + + const sessionCookie2 = await recordPage.getCookie('_splunk_rum_sid') + + expect(sessionCookie2).toBeTruthy() + + const sessionCookie2Parsed = JSON.parse(decodeURIComponent(sessionCookie2.value)) + + expect(sessionCookie1Parsed.expiresAt).toBe(sessionCookie2Parsed.expiresAt) + + expect(recordPage.receivedSpans.filter((s) => s.name === 'guard-span')).toHaveLength(1) + }) + + test('all spans extend session', async ({ recordPage }) => { + await recordPage.goTo('/extend-activity/all.ejs') + + const sessionCookie1 = await recordPage.getCookie('_splunk_rum_sid') + + expect(sessionCookie1).toBeTruthy() + + const sessionCookie1Parsed = JSON.parse(decodeURIComponent(sessionCookie1.value)) + + await recordPage.waitForTimeout(1500) + + await recordPage.evaluate(() => { + ;(window as any).SplunkRum.provider.getTracer('guard').startSpan('guard-span').end() + }) + + await recordPage.waitForTimeout(1500) + + const sessionCookie2 = await recordPage.getCookie('_splunk_rum_sid') + + expect(sessionCookie2).toBeTruthy() + + const sessionCookie2Parsed = JSON.parse(decodeURIComponent(sessionCookie2.value)) + + expect(sessionCookie1Parsed.expiresAt).toBeLessThan(sessionCookie2Parsed.expiresAt) + + expect(recordPage.receivedSpans.filter((s) => s.name === 'guard-span')).toHaveLength(1) + }) +}) diff --git a/packages/integration-tests/src/tests/extend-activity/some.ejs b/packages/integration-tests/src/tests/extend-activity/some.ejs new file mode 100644 index 00000000..8a3baee1 --- /dev/null +++ b/packages/integration-tests/src/tests/extend-activity/some.ejs @@ -0,0 +1,20 @@ + + + + + All spans extend session + + <%- renderAgent() %> + + +

All spans extend session to false

+ +

+  
+  
+
+
diff --git a/packages/integration-tests/src/tests/long-task/index.ejs b/packages/integration-tests/src/tests/long-task/index.ejs
index 5a096010..37a60f02 100644
--- a/packages/integration-tests/src/tests/long-task/index.ejs
+++ b/packages/integration-tests/src/tests/long-task/index.ejs
@@ -19,7 +19,7 @@
     };
 
     document.querySelector('#btnLongtask').addEventListener('click', () => {
-      window.testing = true;
+	  window.testing = true;
       generateLongTask();
       window.testing = false;
     });
diff --git a/packages/integration-tests/src/tests/long-task/long-task.spec.ts b/packages/integration-tests/src/tests/long-task/long-task.spec.ts
index 9546fa09..b1d6114e 100644
--- a/packages/integration-tests/src/tests/long-task/long-task.spec.ts
+++ b/packages/integration-tests/src/tests/long-task/long-task.spec.ts
@@ -87,4 +87,111 @@ test.describe('long task', () => {
 
 		expect(longTaskSpans).toHaveLength(0)
 	})
+
+	test('longtask will spawn new session', async ({ recordPage, browserName }) => {
+		if (browserName === 'webkit' || browserName === 'firefox') {
+			test.skip()
+		}
+
+		await recordPage.goTo(
+			'/long-task/index.ejs?_experimental_longtaskNoStartSession=false&disableInstrumentation=connectivity,document,errors,fetch,interactions,postload,socketio,visibility,websocket,webvitals,xhr',
+		)
+
+		await recordPage.locator('#btnLongtask').click()
+
+		const sessionCookie1 = await recordPage.getCookie('_splunk_rum_sid')
+		expect(sessionCookie1).toBeTruthy()
+
+		const sessionCookie1Parsed = JSON.parse(decodeURIComponent(sessionCookie1.value))
+
+		await recordPage.waitForTimeoutAndFlushData(1000)
+
+		const allSpans1 = recordPage.receivedSpans
+
+		expect(allSpans1).toHaveLength(1)
+
+		// Set session as expired using expiresAt
+		await recordPage.evaluate(
+			([expiresAt, id, startTime]) => {
+				globalThis[Symbol.for('opentelemetry.js.api.1')]['splunk.rum']['store'].set({
+					expiresAt,
+					id,
+					startTime,
+				})
+			},
+			[Date.now(), sessionCookie1Parsed.id, sessionCookie1Parsed.startTime],
+		)
+
+		await recordPage.locator('#btnLongtask').click()
+
+		await recordPage.waitForTimeoutAndFlushData(1000)
+
+		const sessionCookie3 = await recordPage.getCookie('_splunk_rum_sid')
+		expect(sessionCookie3).toBeTruthy()
+
+		const allSpans2 = recordPage.receivedSpans
+		expect(allSpans2).toHaveLength(2)
+
+		const sessionCookie3Parsed = JSON.parse(decodeURIComponent(sessionCookie3.value))
+
+		expect(sessionCookie1Parsed.id).not.toBe(sessionCookie3Parsed.id)
+	})
+
+	test('longtask will not spawn new session', async ({ recordPage, browserName }) => {
+		if (browserName === 'webkit' || browserName === 'firefox') {
+			test.skip()
+		}
+
+		await recordPage.goTo(
+			'/long-task/index.ejs?_experimental_longtaskNoStartSession=true&disableInstrumentation=connectivity,document,errors,fetch,interactions,postload,socketio,visibility,websocket,webvitals,xhr',
+		)
+
+		await recordPage.locator('#btnLongtask').click()
+
+		const sessionCookie1 = await recordPage.getCookie('_splunk_rum_sid')
+
+		expect(sessionCookie1).toBeTruthy()
+
+		const sessionCookie1Parsed = JSON.parse(decodeURIComponent(sessionCookie1.value))
+
+		await recordPage.waitForTimeoutAndFlushData(1000)
+
+		const allSpans1 = recordPage.receivedSpans
+
+		expect(allSpans1).toHaveLength(1)
+
+		const onlyLongTask = allSpans1[0]
+
+		// Set session as expired using expiresAt
+		await recordPage.evaluate(
+			([expiresAt, id, startTime]) => {
+				globalThis[Symbol.for('opentelemetry.js.api.1')]['splunk.rum']['store'].set({
+					expiresAt,
+					id,
+					startTime,
+				})
+			},
+			[Date.now(), sessionCookie1Parsed.id, sessionCookie1Parsed.startTime],
+		)
+
+		await recordPage.waitForTimeout(1000)
+
+		await recordPage.locator('#btnLongtask').click()
+
+		await recordPage.waitForTimeoutAndFlushData(1000)
+
+		const allSpans2 = recordPage.receivedSpans
+
+		expect(allSpans2).toHaveLength(1)
+
+		expect(allSpans2[0].id).toBe(onlyLongTask.id)
+
+		const sessionCookie2 = await recordPage.getCookie('_splunk_rum_sid')
+
+		expect(sessionCookie2).toBeTruthy()
+
+		const sessionCookie2Parsed = JSON.parse(decodeURIComponent(sessionCookie1.value))
+
+		expect(sessionCookie2Parsed.id).toBe(sessionCookie1Parsed.id)
+	})
 })
diff --git a/packages/integration-tests/src/tests/sampling/index.ejs b/packages/integration-tests/src/tests/sampling/sampling.ejs
similarity index 71%
rename from packages/integration-tests/src/tests/sampling/index.ejs
rename to packages/integration-tests/src/tests/sampling/sampling.ejs
index d5ccd8c0..f9555a22 100644
--- a/packages/integration-tests/src/tests/sampling/index.ejs
+++ b/packages/integration-tests/src/tests/sampling/sampling.ejs
@@ -3,9 +3,6 @@
 
 	
 	Session sampling
-	
 	<%- renderAgent() %>
 
 
diff --git a/packages/integration-tests/src/tests/sampling/sampling.spec.ts b/packages/integration-tests/src/tests/sampling/sampling.spec.ts
index 20bae3bb..be5e317a 100644
--- a/packages/integration-tests/src/tests/sampling/sampling.spec.ts
+++ b/packages/integration-tests/src/tests/sampling/sampling.spec.ts
@@ -20,45 +20,42 @@ import { test } from '../../utils/test'
 
 test.describe('sampling', () => {
 	test('all spans arrive if ratio is 1', async ({ recordPage }) => {
-		await recordPage.goTo('/sampling/index.ejs?samplingRatio=1')
+		await recordPage.goTo('/sampling/sampling.ejs?samplingRatio=1')
 		await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'guard-span').length === 1)
 	})
 
 	test('no spans arrive if ratio is 0', async ({ recordPage }) => {
-		await recordPage.goTo('/sampling/index.ejs?samplingRatio=0')
+		await recordPage.goTo('/sampling/sampling.ejs?samplingRatio=0')
 		await recordPage.waitForTimeout(1000)
 
 		expect(recordPage.receivedSpans).toHaveLength(0)
 	})
 
 	test('all spans arrive if session was sampled', async ({ recordPage }) => {
-		await recordPage.goTo('/sampling/index.ejs?samplingRatio=0.99&forceSessionId=a0000000000000000000000000000000')
+		await recordPage.goTo(
+			'/sampling/sampling.ejs?samplingRatio=0.99&forceSessionId=a0000000000000000000000000000000',
+		)
 		await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'guard-span').length === 1)
-	})
 
-	test('no spans arrive if session was not sampled', async ({ recordPage }) => {
-		await recordPage.goTo('/sampling/index.ejs?samplingRatio=0.01&forceSessionId=a0000000000000000000000000000000')
-		await recordPage.waitForTimeout(1000)
+		const sessionCookieEncoded = await recordPage.getCookie('_splunk_rum_sid')
+		expect(sessionCookieEncoded).toBeTruthy()
 
-		expect(recordPage.receivedSpans).toHaveLength(0)
+		const sessionCookieRaw = decodeURIComponent(sessionCookieEncoded.value)
+		expect(JSON.parse(sessionCookieRaw).id).toBe('a0000000000000000000000000000000')
 	})
 
-	test('all spans arrive if session was sampled even if longtasks do not start a new session', async ({
-		recordPage,
-	}) => {
+	test('no spans arrive if session was not sampled', async ({ recordPage }) => {
 		await recordPage.goTo(
-			'/sampling/index.ejs?samplingRatio=0.99&forceSessionId=a0000000000000000000000000000000&_experimental_longtaskNoStartSession=true',
+			'/sampling/sampling.ejs?samplingRatio=0.01&forceSessionId=a0000000000000000000000000000000',
 		)
+		await recordPage.waitForTimeout(1000)
 
-		// spans have arrived
-		await recordPage.waitForSpans((spans) => spans.filter((span) => span.name === 'guard-span').length === 1)
+		expect(recordPage.receivedSpans).toHaveLength(0)
 
-		// we have a cookie
 		const sessionCookieEncoded = await recordPage.getCookie('_splunk_rum_sid')
 		expect(sessionCookieEncoded).toBeTruthy()
 
-		// and the session is active
 		const sessionCookieRaw = decodeURIComponent(sessionCookieEncoded.value)
-		expect(JSON.parse(sessionCookieRaw).inactive).toBeFalsy()
+		expect(JSON.parse(sessionCookieRaw).id).toBe('a0000000000000000000000000000000')
 	})
 })
diff --git a/packages/web/src/global-utils.ts b/packages/web/src/global-utils.ts
index 3e7a8756..15622cb7 100644
--- a/packages/web/src/global-utils.ts
+++ b/packages/web/src/global-utils.ts
@@ -21,13 +21,16 @@ import { VERSION } from './version'
 
 const GLOBAL_OPENTELEMETRY_API_KEY = Symbol.for('opentelemetry.js.api.1')
 
+const GLOBAL_SPLUNK_RUM_KEY = 'splunk.rum'
+
+const GLOBAL_SPLUNK_RUM_VERSION_KEY = `${GLOBAL_SPLUNK_RUM_KEY}.version`
 /**
  * otel-api's global function. This isn't exported by otel/api but would be
  * super useful to register global components for experimental purposes...
  * For us, it's included to register components accessed by other packages,
  * eg. sharing session id manager with session recorder
  */
-export function registerGlobal(type: string, instance: unknown, allowOverride = false): boolean {
+export function registerGlobal(instance: unknown, allowOverride = false): boolean {
 	if (!globalThis[GLOBAL_OPENTELEMETRY_API_KEY]) {
 		diag.error('SplunkRum: Tried to access global before otel setup')
 		return false
@@ -35,37 +38,44 @@ export function registerGlobal(type: string, instance: unknown, allowOverride =
 
 	const api = globalThis[GLOBAL_OPENTELEMETRY_API_KEY]
 
-	if (!api['splunk.rum.version']) {
-		api['splunk.rum.version'] = VERSION
+	if (!api[GLOBAL_SPLUNK_RUM_VERSION_KEY]) {
+		api[GLOBAL_SPLUNK_RUM_VERSION_KEY] = VERSION
 	}
 
-	if (api['splunk.rum.version'] !== VERSION) {
+	if (api[GLOBAL_SPLUNK_RUM_VERSION_KEY] !== VERSION) {
 		diag.error(`SplunkRum: Global: Multiple versions detected (${VERSION} already registered)`)
 		return false
 	}
 
-	if (!allowOverride && api[type]) {
-		diag.error(`SplunkRum: Attempted duplicate registration of otel API ${type}`)
+	if (!allowOverride && api[GLOBAL_SPLUNK_RUM_KEY]) {
+		diag.error(`SplunkRum: Attempted duplicate registration of otel API ${GLOBAL_SPLUNK_RUM_KEY}`)
 		return false
 	}
 
-	api[type] = instance
+	api[GLOBAL_SPLUNK_RUM_KEY] = instance
 	return true
 }
 
-export function unregisterGlobal(type: string): boolean {
+export function unregisterGlobal(): boolean {
 	const api = globalThis[GLOBAL_OPENTELEMETRY_API_KEY]
 	if (!api) {
-		diag.warn(`OTel API ref was missing while trying to unregister ${type}`)
+		diag.warn(
+			`OTel API ref was missing while trying to unregister ${GLOBAL_SPLUNK_RUM_KEY} or ${GLOBAL_SPLUNK_RUM_VERSION_KEY}`,
+		)
 		return false
 	}
 
-	if (!!api['splunk.rum.version'] && api['splunk.rum.version'] !== VERSION) {
+	if (!!api[GLOBAL_SPLUNK_RUM_VERSION_KEY] && api[GLOBAL_SPLUNK_RUM_VERSION_KEY] !== VERSION) {
 		diag.warn(
-			`SplunkRum version in OTel API ref (${api['splunk.rum.version']}) didn't match our version (${VERSION}).`,
+			`SplunkRum version in OTel API ref (${api[GLOBAL_SPLUNK_RUM_VERSION_KEY]}) didn't match our version (${VERSION}).`,
 		)
 	}
 
-	delete api[type]
+	delete api[GLOBAL_SPLUNK_RUM_KEY]
+	delete api[GLOBAL_SPLUNK_RUM_VERSION_KEY]
 	return true
 }
+
+export function getGlobal(): Type | undefined {
+	return globalThis[GLOBAL_OPENTELEMETRY_API_KEY]?.[GLOBAL_SPLUNK_RUM_KEY]
+}
diff --git a/packages/web/src/index.ts b/packages/web/src/index.ts
index 30fdc519..52e4959f 100644
--- a/packages/web/src/index.ts
+++ b/packages/web/src/index.ts
@@ -291,7 +291,7 @@ export const SplunkRum: SplunkOtelWebType = {
 		// touches otel globals, our registerGlobal requires this first
 		diag.setLogger(new DiagConsoleLogger(), options?.debug ? DiagLogLevel.DEBUG : DiagLogLevel.WARN)
 
-		const registered = registerGlobal('splunk.rum', this)
+		const registered = registerGlobal(this)
 		if (!registered) {
 			return
 		}
@@ -499,8 +499,7 @@ export const SplunkRum: SplunkOtelWebType = {
 		eventTarget = undefined
 
 		diag.disable()
-		unregisterGlobal('splunk.rum')
-		unregisterGlobal('splunk.rum.version')
+		unregisterGlobal()
 
 		inited = false
 	},
diff --git a/packages/web/src/session/session.ts b/packages/web/src/session/session.ts
index 85fb1b32..d9427ca4 100644
--- a/packages/web/src/session/session.ts
+++ b/packages/web/src/session/session.ts
@@ -24,6 +24,7 @@ import { SESSION_INACTIVITY_TIMEOUT_MS, SESSION_STORAGE_KEY } from './constants'
 import { isSessionDurationExceeded, isSessionInactivityTimeoutReached, isSessionState } from './utils'
 import { PersistenceType } from '../types'
 import { buildStore, Store } from '../storage/store'
+import { getGlobal } from '../global-utils'
 
 /*
     The basic idea is to let the browser expire cookies for us "naturally" once
@@ -53,8 +54,8 @@ export function markActivity(): void {
 }
 
 function generateSessionId(id?: string): string {
-	if (window['__integrationTestSessionId']) {
-		return window['__integrationTestSessionId']
+	if (window['__splunkRumIntegrationTestSessionId']) {
+		return window['__splunkRumIntegrationTestSessionId']
 	}
 
 	if (id) {
@@ -85,7 +86,7 @@ function getStore(): Store {
 		return str
 	}
 
-	throw new Error('Session store was accessed but not initialised.')
+	throw new Error('Session store was accessed but not initialized.')
 }
 
 export function getOrInitInactiveSession(): SessionState {
@@ -102,11 +103,11 @@ export function getCurrentSessionState({ forceDiskRead = false }): SessionState
 	const sessionState = getStore().get({ forceDiskRead })
 
 	if (!isSessionState(sessionState)) {
-		return
+		return undefined
 	}
 
 	if (isSessionDurationExceeded(sessionState) || isSessionInactivityTimeoutReached(sessionState)) {
-		return
+		return undefined
 	}
 
 	return sessionState
@@ -242,6 +243,12 @@ export function initSessionTracking(
 
 	updateSessionStatus({ forceStore: true })
 
+	// TODO: For integration tests, find better solution
+	const SplunkRum = getGlobal()
+	if (SplunkRum) {
+		SplunkRum['store'] = store
+	}
+
 	return {
 		deinit: () => {
 			ACTIVITY_EVENTS.forEach((type) => document.removeEventListener(type, markActivity))