Skip to content

Commit

Permalink
feat: test
Browse files Browse the repository at this point in the history
  • Loading branch information
Joozty committed Dec 5, 2024
1 parent b69de2c commit 61d0d99
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 44 deletions.
8 changes: 7 additions & 1 deletion packages/web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ export interface SplunkOtelWebType extends SplunkOtelWebEventTarget {
/* Used internally by the SplunkSessionRecorder - span from session can extend the session */
_internalOnExternalSpanCreated: () => void

_processedOptions: SplunkOtelWebConfigInternal | null

attributesProcessor?: SplunkSpanAttributesProcessor

deinit: (force?: boolean) => void
Expand Down Expand Up @@ -242,6 +244,8 @@ export const SplunkRum: SplunkOtelWebType = {
ParentBasedSampler,
SessionBasedSampler,

_processedOptions: null,

get inited(): boolean {
return inited
},
Expand Down Expand Up @@ -293,6 +297,8 @@ export const SplunkRum: SplunkOtelWebType = {
},
)

this._processedOptions = processedOptions

if (processedOptions.realm) {
if (!processedOptions.beaconEndpoint) {
processedOptions.beaconEndpoint = getBeaconEndpointForRealm(processedOptions)
Expand Down Expand Up @@ -511,7 +517,7 @@ export const SplunkRum: SplunkOtelWebType = {
},

_internalOnExternalSpanCreated() {
updateSessionStatus()
updateSessionStatus({ forceStore: false, useLocalStorage: this.processedOptions.useLocalStorage ?? false })
},
}

Expand Down
16 changes: 12 additions & 4 deletions packages/web/src/session/cookie-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,19 @@ import { throttle } from '../utils/throttle'

export const cookieStore = {
cachedValue: null,
set: throttle((value: string) => {
document.cookie = value
set: (value: string) => {
cookieStore.cachedValue = value
cookieStore._set(value)
},

_set: throttle((value: string) => {
document.cookie = value
}, 1000),

flush: () => {
cookieStore._set.flush()
},

get: ({ forceStoreRead }: { forceStoreRead: boolean }): string => {
if (cookieStore.cachedValue === null || forceStoreRead) {
cookieStore.cachedValue = document.cookie
Expand Down Expand Up @@ -93,15 +101,15 @@ export function renewCookieTimeout(

cookieStore.set(cookie)
if (forceStoreWrite) {
cookieStore.set.flush()
cookieStore.flush()
}
}

export function clearSessionCookie(cookieDomain?: string): void {
const domain = cookieDomain ? `domain=${cookieDomain};` : ''
const cookie = `${SESSION_STORAGE_KEY}=;domain=${domain};expires=Thu, 01 Jan 1970 00:00:00 GMT`
cookieStore.set(cookie)
cookieStore.set.flush()
cookieStore.flush()
}

export function findCookieValue(
Expand Down
46 changes: 30 additions & 16 deletions packages/web/src/session/local-storage-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,31 @@ import { isSessionDurationExceeded, isSessionInactivityTimeoutReached, isSession
import { throttle } from '../utils/throttle'

export const localStore = {
cachedValue: {},
set: throttle((key: string, value: string) => {
safelySetLocalStorage(key, value)
cachedValue: undefined,
set: (value: string) => {
localStore.cachedValue = value
localStore._set(value)
},

flush: () => {
localStore._set.flush()
},

_set: throttle((value: string) => {
safelySetLocalStorage(SESSION_STORAGE_KEY, value)
}, 1000),

get: (key: string, { forceStoreRead }: { forceStoreRead: boolean }): string => {
if (localStore.cachedValue[key] === undefined || forceStoreRead) {
localStore.cachedValue[key] = safelyGetLocalStorage(key)
return localStore.cachedValue[key]
get: ({ forceStoreRead }: { forceStoreRead: boolean }): string => {
if (localStore.cachedValue === undefined || forceStoreRead) {
localStore.cachedValue = safelyGetLocalStorage(SESSION_STORAGE_KEY)
return localStore.cachedValue
}

return localStore.cachedValue[key]
return localStore.cachedValue
},
remove: (key: string) => {
safelyRemoveFromLocalStorage(key)
localStore.cachedValue[key] = undefined
remove: () => {
safelyRemoveFromLocalStorage(SESSION_STORAGE_KEY)
localStore.cachedValue = undefined
},
}

Expand All @@ -49,7 +57,7 @@ export const getSessionStateFromLocalStorage = ({
}): SessionState | undefined => {
let sessionState: unknown = undefined
try {
sessionState = JSON.parse(localStore.get(SESSION_STORAGE_KEY, { forceStoreRead }))
sessionState = JSON.parse(localStore.get({ forceStoreRead }))
} catch {
return undefined
}
Expand All @@ -58,21 +66,27 @@ export const getSessionStateFromLocalStorage = ({
return
}

if (!isSessionDurationExceeded(sessionState) || isSessionInactivityTimeoutReached(sessionState)) {
if (isSessionDurationExceeded(sessionState) || isSessionInactivityTimeoutReached(sessionState)) {
return
}

return sessionState
}

export const setSessionStateToLocalStorage = (sessionState: SessionState): void => {
export const setSessionStateToLocalStorage = (
sessionState: SessionState,
{ forceStoreWrite }: { forceStoreWrite: boolean },
): void => {
if (isSessionDurationExceeded(sessionState)) {
return
}

localStore.set(SESSION_STORAGE_KEY, JSON.stringify(sessionState))
localStore.set(JSON.stringify(sessionState))
if (forceStoreWrite) {
localStore.flush()
}
}

export const clearSessionStateFromLocalStorage = (): void => {
localStore.remove(SESSION_STORAGE_KEY)
localStore.remove()
}
28 changes: 20 additions & 8 deletions packages/web/src/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,14 @@ export function getCurrentSessionState({ useLocalStorage = false, forceStoreRead
// 1) Check if the cookie has been expired by the browser; if so, create a new one
// 2) If activity has occurred since the last periodic invocation, renew the cookie timeout
// (Only exported for testing purposes.)
export function updateSessionStatus(useLocalStorage = false): void {
let sessionState = getCurrentSessionState({ useLocalStorage, forceStoreRead: false })
export function updateSessionStatus({
forceStore,
useLocalStorage = false,
}: {
forceStore: boolean
useLocalStorage: boolean
}): void {
let sessionState = getCurrentSessionState({ useLocalStorage, forceStoreRead: forceStore })
let shouldForceWrite = false
if (!sessionState) {
// Check if another tab has created a new session
Expand All @@ -91,9 +97,9 @@ export function updateSessionStatus(useLocalStorage = false): void {
if (recentActivity) {
sessionState.expiresAt = Date.now() + SESSION_INACTIVITY_TIMEOUT_MS
if (useLocalStorage) {
setSessionStateToLocalStorage(sessionState)
setSessionStateToLocalStorage(sessionState, { forceStoreWrite: shouldForceWrite || forceStore })
} else {
renewCookieTimeout(sessionState, cookieDomain, { forceStoreWrite: shouldForceWrite })
renewCookieTimeout(sessionState, cookieDomain, { forceStoreWrite: shouldForceWrite || forceStore })
}
}

Expand All @@ -105,7 +111,10 @@ function hasNativeSessionId(): boolean {
}

class SessionSpanProcessor implements SpanProcessor {
constructor(private readonly allSpansAreActivity: boolean) {}
constructor(
private readonly allSpansAreActivity: boolean,
private readonly useLocalStorage: boolean,
) {}

forceFlush(): Promise<void> {
return Promise.resolve()
Expand All @@ -119,7 +128,10 @@ class SessionSpanProcessor implements SpanProcessor {
}

context.with(suppressTracing(context.active()), () => {
updateSessionStatus()
updateSessionStatus({
forceStore: false,
useLocalStorage: this.useLocalStorage,
})
})
}

Expand Down Expand Up @@ -156,9 +168,9 @@ export function initSessionTracking(
eventTarget = newEventTarget

ACTIVITY_EVENTS.forEach((type) => document.addEventListener(type, markActivity, { capture: true, passive: true }))
provider.addSpanProcessor(new SessionSpanProcessor(allSpansAreActivity))
provider.addSpanProcessor(new SessionSpanProcessor(allSpansAreActivity, useLocalStorage))

updateSessionStatus(useLocalStorage)
updateSessionStatus({ useLocalStorage, forceStore: true })

return {
deinit: () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/session/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const isSessionDurationExceeded = (sessionState: SessionState): boolean =
}

export const isSessionInactivityTimeoutReached = (sessionState: SessionState): boolean => {
if (!sessionState.expiresAt) {
if (sessionState.expiresAt === undefined) {
return false
}

Expand Down
21 changes: 17 additions & 4 deletions packages/web/test/SessionBasedSampler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@ import { SessionBasedSampler } from '../src/SessionBasedSampler'
import { initSessionTracking, updateSessionStatus } from '../src/session/session'
import { context, SamplingDecision } from '@opentelemetry/api'
import { SplunkWebTracerProvider } from '../src'
import { SESSION_STORAGE_KEY } from '../src/session/constants'
import { SESSION_INACTIVITY_TIMEOUT_MS, SESSION_STORAGE_KEY } from '../src/session/constants'

describe('Session based sampler', () => {
it('decide sampling based on session id and ratio', () => {
// Session id < target ratio
const lowSessionId = '0'.repeat(32)
const lowCookieValue = encodeURIComponent(JSON.stringify({ id: lowSessionId, startTime: new Date().getTime() }))
const lowCookieValue = encodeURIComponent(
JSON.stringify({
id: lowSessionId,
startTime: new Date().getTime(),
expiresAt: new Date().getTime() + SESSION_INACTIVITY_TIMEOUT_MS,
}),
)
document.cookie = SESSION_STORAGE_KEY + '=' + lowCookieValue + '; path=/; max-age=' + 10
const provider = new SplunkWebTracerProvider()
initSessionTracking(provider, lowSessionId, new InternalEventTarget())
Expand All @@ -43,10 +49,17 @@ describe('Session based sampler', () => {
// Session id > target ratio
const highSessionId = '1234567890abcdeffedcba0987654321'
const highCookieValue = encodeURIComponent(
JSON.stringify({ id: highSessionId, startTime: new Date().getTime() }),
JSON.stringify({
id: highSessionId,
startTime: new Date().getTime(),
expiresAt: new Date().getTime() + SESSION_INACTIVITY_TIMEOUT_MS,
}),
)
document.cookie = SESSION_STORAGE_KEY + '=' + highCookieValue + '; path=/; max-age=' + 10
updateSessionStatus()
updateSessionStatus({
forceStore: true,
useLocalStorage: false,
})

assert.strictEqual(
sampler.shouldSample(context.active(), '0000000000000000', 'test', 0, {}, []).decision,
Expand Down
2 changes: 1 addition & 1 deletion packages/web/test/SplunkOtelWeb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe('SplunkOtelWeb', () => {
})

document.body.click()
updateSessionStatus()
updateSessionStatus({ forceStore: false, useLocalStorage: false })

// Wait for promise chain to resolve
await Promise.resolve()
Expand Down
2 changes: 1 addition & 1 deletion packages/web/test/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { VERSION } from '../src/version'
function doesBeaconUrlEndWith(suffix) {
const sps = (SplunkRum.provider.getActiveSpanProcessor() as any)._spanProcessors
// TODO: refactor to make beaconUrl field private
const beaconUrl = sps[1]._exporter.beaconUrl || sps[1]._exporter.url
const beaconUrl = sps[2]._exporter.beaconUrl || sps[2]._exporter.url
assert.ok(beaconUrl.endsWith(suffix), `Checking beaconUrl if (${beaconUrl}) ends with ${suffix}`)
}

Expand Down
26 changes: 18 additions & 8 deletions packages/web/test/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { InternalEventTarget } from '../src/EventTarget'
import { initSessionTracking, getRumSessionId, updateSessionStatus } from '../src/session/session'
import { SplunkWebTracerProvider } from '../src'
import sinon from 'sinon'
import { SESSION_STORAGE_KEY } from '../src/session/constants'
import { SESSION_STORAGE_KEY, SESSION_INACTIVITY_TIMEOUT_MS } from '../src/session/constants'
import { clearSessionCookie, cookieStore } from '../src/session/cookie-session'
import { clearSessionStateFromLocalStorage } from '../src/session/local-storage-session'

Expand All @@ -41,14 +41,20 @@ describe('Session tracking', () => {
const firstSessionId = getRumSessionId()
assert.strictEqual(firstSessionId.length, 32)
// no marked activity, should keep same state
updateSessionStatus()
updateSessionStatus({ forceStore: false, useLocalStorage: false })
assert.strictEqual(firstSessionId, getRumSessionId())
// set cookie to expire in 2 seconds, mark activity, and then updateSessionStatus.
// Wait 4 seconds and cookie should still be there (having been renewed)
const cookieValue = encodeURIComponent(JSON.stringify({ id: firstSessionId, startTime: new Date().getTime() }))
const cookieValue = encodeURIComponent(
JSON.stringify({
id: firstSessionId,
startTime: new Date().getTime(),
expiresAt: new Date().getTime() + SESSION_INACTIVITY_TIMEOUT_MS,
}),
)
document.cookie = SESSION_STORAGE_KEY + '=' + cookieValue + '; path=/; max-age=' + 2
document.body.dispatchEvent(new Event('click'))
updateSessionStatus()
updateSessionStatus({ forceStore: false, useLocalStorage: false })
setTimeout(() => {
// because of activity, same session should be there
assert.ok(document.cookie.includes(SESSION_STORAGE_KEY))
Expand All @@ -58,11 +64,15 @@ describe('Session tracking', () => {
// after max age code does its thing
const fiveHoursMillis = 5 * 60 * 60 * 1000
const tooOldCookieValue = encodeURIComponent(
JSON.stringify({ id: firstSessionId, startTime: new Date().getTime() - fiveHoursMillis }),
JSON.stringify({
id: firstSessionId,
startTime: new Date().getTime() - fiveHoursMillis,
expiresAt: new Date().getTime() + SESSION_INACTIVITY_TIMEOUT_MS - fiveHoursMillis,
}),
)
document.cookie = SESSION_STORAGE_KEY + '=' + tooOldCookieValue + '; path=/; max-age=' + 4

updateSessionStatus()
updateSessionStatus({ forceStore: true, useLocalStorage: false })
assert.ok(document.cookie.includes(SESSION_STORAGE_KEY))
const newSessionId = getRumSessionId()
assert.strictEqual(newSessionId.length, 32)
Expand All @@ -84,7 +94,7 @@ describe('Session tracking', () => {
initSessionTracking(provider, firstSessionId, new InternalEventTarget(), undefined, allSpansAreActivity)

provider.getTracer('tracer').startSpan('any-span').end()
updateSessionStatus()
updateSessionStatus({ forceStore: false, useLocalStorage: false })
}

it('non-activity spans do not trigger a new session', (done) => {
Expand Down Expand Up @@ -129,7 +139,7 @@ describe('Session tracking - localStorage', () => {
)

const firstSessionId = getRumSessionId()
updateSessionStatus(useLocalStorage)
updateSessionStatus({ forceStore: true, useLocalStorage })
assert.strictEqual(firstSessionId, getRumSessionId())

trackingHandle.deinit()
Expand Down

0 comments on commit 61d0d99

Please sign in to comment.