Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: session replay module #923

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 0 additions & 47 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/session-recorder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"@opentelemetry/core": "^1.25.1",
"@opentelemetry/resources": "^1.25.1",
"fflate": "^0.8.0",
"rrweb": "^1.1.3",
"type-fest": "^4.0.0"
},
"peerDependencies": {
Expand Down
122 changes: 54 additions & 68 deletions packages/session-recorder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
*/

import { ProxyTracerProvider, TracerProvider, trace, Tracer } from '@opentelemetry/api'
import { record } from 'rrweb'
import OTLPLogExporter from './OTLPLogExporter'
import { BatchLogProcessor, convert } from './BatchLogProcessor'
import { VERSION } from './version'
Expand All @@ -26,26 +25,16 @@ import { getGlobal } from './utils'
import type { Resource } from '@opentelemetry/resources'
import type { SplunkOtelWebType } from '@splunk/otel-web'

import { loadRecorderBrowserScript } from './session-replay/load'

interface BasicTracerProvider extends TracerProvider {
readonly resource: Resource
}

type RRWebOptions = Parameters<typeof record>[0]

export type SplunkRumRecorderConfig = RRWebOptions & {
/**
* @deprecated Use RUM token in rumAccessToken
*/
apiToken?: string

export type SplunkRumRecorderConfig = {
/** Destination for the captured data */
beaconEndpoint?: string

/** Destination for the captured data
* @deprecated Use beaconEndpoint
*/
beaconUrl?: string

/** Debug mode */
debug?: boolean

Expand All @@ -59,33 +48,6 @@ export type SplunkRumRecorderConfig = RRWebOptions & {
* with only RUM scope as it's visible to every user of your app
**/
rumAccessToken?: string

/**
* RUM authorization token for data sending. Please make sure this is a token
* with only RUM scope as it's visible to every user of your app
* @deprecated Renamed to `rumAccessToken`
**/
rumAuth?: string
}

function migrateConfigOption(
config: SplunkRumRecorderConfig,
from: keyof SplunkRumRecorderConfig,
to: keyof SplunkRumRecorderConfig,
) {
if (from in config && !(to in config)) {
// @ts-expect-error There's no way to type this right
config[to] = config[from]
}
}

/**
* Update configuration based on configuration option renames
*/
function migrateConfig(config: SplunkRumRecorderConfig) {
migrateConfigOption(config, 'beaconUrl', 'beaconEndpoint')
migrateConfigOption(config, 'rumAuth', 'rumAccessToken')
return config
}

// Hard limit of 4 hours of maximum recording during one session
Expand All @@ -94,20 +56,22 @@ const MAX_CHUNK_SIZE = 950 * 1024 // ~950KB
const encoder = new TextEncoder()
const decoder = new TextDecoder()

let inited: (() => void) | false | undefined = false
let inited: true | false | undefined = false
let tracer: Tracer
let lastKnownSession: string
let sessionStartTime = 0
let paused = false
let eventCounter = 1
let logCounter = 1

let srp: any

const SplunkRumRecorder = {
get inited(): boolean {
return Boolean(inited)
},

init(config: SplunkRumRecorderConfig): void {
async init(config: SplunkRumRecorderConfig): Promise<void> {
if (inited) {
return
}
Expand All @@ -117,6 +81,8 @@ const SplunkRumRecorder = {
return
}

await loadRecorderBrowserScript()

const SplunkRum = getGlobal<SplunkOtelWebType>('splunk.rum')

let tracerProvider: BasicTracerProvider | ProxyTracerProvider = trace.getTracerProvider() as BasicTracerProvider
Expand All @@ -125,15 +91,13 @@ const SplunkRumRecorder = {
}

if (!SplunkRum || !SplunkRum.resource) {
console.error('Splunk OTEL Web must be inited before session recorder.')
console.error('Splunk OTEL Web must be initiated before session recorder.')
return
}

const resource = SplunkRum.resource

migrateConfig(config)

const { apiToken, beaconEndpoint, debug, realm, rumAccessToken, ...rrwebConf } = config
const { beaconEndpoint, debug, realm, rumAccessToken } = config
tracer = trace.getTracer('splunk.rr-web', VERSION)
const span = tracer.startSpan('record init')

Expand All @@ -160,19 +124,13 @@ const SplunkRumRecorder = {
return
}

const headers = {}
if (apiToken) {
headers['X-SF-Token'] = apiToken
}

if (rumAccessToken) {
exportUrl += `?auth=${rumAccessToken}`
}

const exporter = new OTLPLogExporter({
beaconUrl: exportUrl,
debug,
headers,
getResourceAttributes() {
return {
...resource.attributes,
Expand All @@ -183,13 +141,30 @@ const SplunkRumRecorder = {
const processor = new BatchLogProcessor(exporter, {})

lastKnownSession = SplunkRum.getSessionId()

if (SplunkRum.isNewSessionId()) {
console.log('SplunkRum.isNewSessionId()', 'clearing')
;(window as any).SessionReplayPlain.clear()
}

sessionStartTime = Date.now()

inited = record({
maskAllInputs: true,
maskTextSelector: '*',
...rrwebConf,
emit(event) {
srp = new (window as any).SessionReplayPlain({
features: {
backgroundServiceSrc: 'https://domain.com/web/latest/background-service.html',
cacheAssets: false,
iframes: false,
imageBitmap: false,
packAssets: false,
},
isDebug: true,
logLevel: 'debug',
maskAllInputs: false,
maskAllText: false,
maxExportIntervalMs: 5000,
onSegment: (segment) => {
console.log('Have segment', segment)

if (paused) {
return
}
Expand All @@ -211,23 +186,26 @@ const SplunkRumRecorder = {
// reset counters
eventCounter = 1
logCounter = 1
record.takeFullSnapshot()
srp?.stop()
console.log('onSegment - Clearing assets')
;(window as any).SessionReplayPlain.clear()
srp?.start()
}

if (event.timestamp > sessionStartTime + MAX_RECORDING_LENGTH) {
if (segment.metadata.startUnixMs > sessionStartTime + MAX_RECORDING_LENGTH) {
return
}

const time = event.timestamp
const time = Math.floor(segment.metadata.startUnixMs)
const eventI = eventCounter
eventCounter += 1

// Research found that stringifying the rr-web event here is
// more efficient for otlp + gzip exporting
eventCounter += 1

// Blob is unicode aware for size calculation (eg emoji.length = 1 vs blob.size() = 4)
const body = encoder.encode(JSON.stringify(event))
const body = encoder.encode(JSON.stringify({ data: segment.data, metadata: segment.metadata }))
const totalC = Math.ceil(body.byteLength / MAX_CHUNK_SIZE)

console.log('TotalC:', totalC)

for (let i = 0; i < totalC; i++) {
const start = i * MAX_CHUNK_SIZE
const end = (i + 1) * MAX_CHUNK_SIZE
Expand All @@ -237,7 +215,9 @@ const SplunkRumRecorder = {
'rr-web.chunk': i + 1,
'rr-web.total-chunks': totalC,
})

logCounter += 1

if (debug) {
console.log(log)
}
Expand All @@ -246,7 +226,12 @@ const SplunkRumRecorder = {
}
},
})

console.log('Starting SRP')
srp.start()
inited = true
},

resume(): void {
if (!inited) {
return
Expand All @@ -255,7 +240,7 @@ const SplunkRumRecorder = {
const oldPaused = paused
paused = false
if (!oldPaused) {
record.takeFullSnapshot()
srp?.start()
tracer.startSpan('record resume').end()
}
},
Expand All @@ -265,6 +250,7 @@ const SplunkRumRecorder = {
}

if (paused) {
srp.stop()
tracer.startSpan('record stop').end()
}

Expand All @@ -275,7 +261,7 @@ const SplunkRumRecorder = {
return
}

inited()
srp?.stop()
inited = false
},
}
Expand Down
Loading
Loading