Skip to content

Commit

Permalink
feat: add _experimental_longtaskNoStartSession flag
Browse files Browse the repository at this point in the history
  • Loading branch information
Joozty committed Dec 4, 2024
1 parent 767d36e commit 8d70e79
Show file tree
Hide file tree
Showing 17 changed files with 217 additions and 123 deletions.
2 changes: 1 addition & 1 deletion packages/web/src/SessionBasedSampler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import { Context, Link, Sampler, SamplingResult, SpanAttributes, SpanKind } from '@opentelemetry/api'
import { AlwaysOffSampler, AlwaysOnSampler } from '@opentelemetry/core'
import { getRumSessionId } from './session'
import { getRumSessionId } from './session/session'

export interface SessionBasedSamplerConfig {
/**
Expand Down
13 changes: 12 additions & 1 deletion packages/web/src/SplunkLongTaskInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,21 @@
import { InstrumentationBase, InstrumentationConfig } from '@opentelemetry/instrumentation'

import { VERSION } from './version'
import { parseCookieToSessionState } from './session/session'
import { SplunkOtelWebConfig } from './index'

const LONGTASK_PERFORMANCE_TYPE = 'longtask'
const MODULE_NAME = 'splunk-longtask'

export class SplunkLongTaskInstrumentation extends InstrumentationBase {
private _longtaskObserver: PerformanceObserver | undefined

constructor(config: InstrumentationConfig = {}) {
private initOptions: SplunkOtelWebConfig

constructor(config: InstrumentationConfig = {}, initOptions: SplunkOtelWebConfig) {
super(MODULE_NAME, VERSION, Object.assign({}, config))

this.initOptions = initOptions
}

disable(): void {
Expand All @@ -52,6 +58,11 @@ export class SplunkLongTaskInstrumentation extends InstrumentationBase {
init(): void {}

private _createSpanFromEntry(entry: PerformanceEntry) {
if (!!this.initOptions._experimental_longtaskNoStartSession && !parseCookieToSessionState()) {
// session expired, we do not want to spawn new session from long tasks
return
}

const span = this.tracer.startSpan(LONGTASK_PERFORMANCE_TYPE, {
startTime: entry.startTime,
})
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/SplunkSpanAttributesProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import { Attributes } from '@opentelemetry/api'
import { Span, SpanProcessor } from '@opentelemetry/sdk-trace-base'
import { getRumSessionId } from './session'
import { getRumSessionId } from './session/session'

export class SplunkSpanAttributesProcessor implements SpanProcessor {
private readonly _globalAttributes: Attributes
Expand Down
15 changes: 12 additions & 3 deletions packages/web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { SplunkExporterConfig } from './exporters/common'
import { SplunkZipkinExporter } from './exporters/zipkin'
import { ERROR_INSTRUMENTATION_NAME, SplunkErrorInstrumentation } from './SplunkErrorInstrumentation'
import { generateId, getPluginConfig } from './utils'
import { getRumSessionId, initSessionTracking } from './session'
import { getRumSessionId, initSessionTracking } from './session/session'
import { SplunkWebSocketInstrumentation } from './SplunkWebSocketInstrumentation'
import { WebVitalsInstrumentationConfig, initWebVitals } from './webvitals'
import { SplunkLongTaskInstrumentation } from './SplunkLongTaskInstrumentation'
Expand Down Expand Up @@ -116,6 +116,11 @@ export interface SplunkOtelWebConfig {
*/
_experimental_allSpansExtendSession?: boolean

/*
* If enabled, longtask will not start the new session. Defaults to false.
*/
_experimental_longtaskNoStartSession?: boolean

/** Allows http beacon urls */
allowInsecureBeacon?: boolean

Expand Down Expand Up @@ -487,8 +492,12 @@ export const SplunkRum: SplunkOtelWebType = {
const instrumentations = INSTRUMENTATIONS.map(({ Instrument, confKey, disable }) => {
const pluginConf = getPluginConfig(processedOptions.instrumentations[confKey], pluginDefaults, disable)
if (pluginConf) {
// @ts-expect-error Can't mark in any way that processedOptions.instrumentations[confKey] is of specifc config type
const instrumentation = new Instrument(pluginConf)
const instrumentation =
Instrument === SplunkLongTaskInstrumentation
? new Instrument(pluginConf, options)
: // @ts-expect-error Can't mark in any way that processedOptions.instrumentations[confKey] is of specifc config type
new Instrument(pluginConf)

if (confKey === ERROR_INSTRUMENTATION_NAME && instrumentation instanceof SplunkErrorInstrumentation) {
_errorInstrumentation = instrumentation
}
Expand Down
62 changes: 0 additions & 62 deletions packages/web/src/local-storage-session.ts

This file was deleted.

22 changes: 22 additions & 0 deletions packages/web/src/session/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
*
* 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 SESSION_ID_LENGTH = 32
export const SESSION_DURATION_SECONDS = 4 * 60 * 60 // 4 hours
export const SESSION_DURATION_MS = SESSION_DURATION_SECONDS * 1000
export const SESSION_INACTIVITY_TIMEOUT_MS = 15 * 60 * 1000 // 15 minutes
export const SESSION_STORAGE_KEY = '_splunk_rum_sid'
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@
* limitations under the License.
*
*/
import { isIframe } from './utils'
import { SessionState } from './types'

export const COOKIE_NAME = '_splunk_rum_sid'

const CookieSession = 4 * 60 * 60 * 1000 // 4 hours
const InactivityTimeoutSeconds = 15 * 60
import { isIframe } from '../utils'
import { SessionState } from '../types'
import { SESSION_DURATION_SECONDS, SESSION_STORAGE_KEY } from './constants'
import { isSessionDurationExceeded, isSessionInactivityTimeoutReached, isSessionState } from './utils'

export const cookieStore = {
set: (value: string): void => {
Expand All @@ -31,7 +28,7 @@ export const cookieStore = {
}

export function parseCookieToSessionState(): SessionState | undefined {
const rawValue = findCookieValue(COOKIE_NAME)
const rawValue = findCookieValue(SESSION_STORAGE_KEY)
if (!rawValue) {
return undefined
}
Expand All @@ -52,33 +49,26 @@ export function parseCookieToSessionState(): SessionState | undefined {
return undefined
}

// id validity
if (
!sessionState.id ||
typeof sessionState.id !== 'string' ||
!sessionState.id.length ||
sessionState.id.length !== 32
) {
if (isSessionDurationExceeded(sessionState)) {
return undefined
}

// startTime validity
if (!sessionState.startTime || typeof sessionState.startTime !== 'number' || isPastMaxAge(sessionState.startTime)) {
if (isSessionInactivityTimeoutReached(sessionState)) {
return undefined
}

return sessionState
}

export function renewCookieTimeout(sessionState: SessionState, cookieDomain: string | undefined): void {
if (isPastMaxAge(sessionState.startTime)) {
if (isSessionDurationExceeded(sessionState)) {
// safety valve
return
}

const cookieValue = encodeURIComponent(JSON.stringify(sessionState))
const domain = cookieDomain ? `domain=${cookieDomain};` : ''
let cookie = COOKIE_NAME + '=' + cookieValue + '; path=/;' + domain + 'max-age=' + InactivityTimeoutSeconds
let cookie = SESSION_STORAGE_KEY + '=' + cookieValue + '; path=/;' + domain + 'max-age=' + SESSION_DURATION_SECONDS

if (isIframe()) {
cookie += ';SameSite=None; Secure'
Expand All @@ -91,7 +81,7 @@ export function renewCookieTimeout(sessionState: SessionState, cookieDomain: str

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

Expand All @@ -106,19 +96,3 @@ export function findCookieValue(cookieName: string): string | undefined {
}
return undefined
}

function isPastMaxAge(startTime: number): boolean {
const now = Date.now()
return startTime > now || now > startTime + CookieSession
}

function isSessionState(maybeSessionState: unknown): maybeSessionState is SessionState {
return (
typeof maybeSessionState === 'object' &&
maybeSessionState !== null &&
'id' in maybeSessionState &&
typeof maybeSessionState['id'] === 'string' &&
'startTime' in maybeSessionState &&
typeof maybeSessionState['startTime'] === 'number'
)
}
18 changes: 18 additions & 0 deletions packages/web/src/session/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
*
* 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 * from './types'
52 changes: 52 additions & 0 deletions packages/web/src/session/local-storage-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
*
* 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 { SessionState } from '../types'
import { safelyGetLocalStorage, safelySetLocalStorage, safelyRemoveFromLocalStorage } from '../utils/storage'
import { SESSION_STORAGE_KEY } from './constants'
import { isSessionDurationExceeded, isSessionInactivityTimeoutReached, isSessionState } from './utils'

export const getSessionStateFromLocalStorage = (): SessionState | undefined => {
let sessionState: unknown = undefined
try {
sessionState = JSON.parse(safelyGetLocalStorage(SESSION_STORAGE_KEY))
} catch {
return undefined
}

if (!isSessionState(sessionState)) {
return
}

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

return sessionState
}

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

safelySetLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(sessionState))
}

export const clearSessionStateFromLocalStorage = (): void => {
safelyRemoveFromLocalStorage(SESSION_STORAGE_KEY)
}
Loading

0 comments on commit 8d70e79

Please sign in to comment.