Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ObservabilityClient } from './ObservabilityClient'
import { Platform } from 'react-native'
import {
SESSION_RESUME_THRESHOLD_MS,
SESSION_STORAGE_KEY,
} from '../constants/sessions'

describe('ObservabilityClient — stop during async init', () => {
beforeEach(() => {
Expand Down Expand Up @@ -155,4 +159,32 @@ describe('ObservabilityClient — platform attributes on the Resource', () => {
'device.model.identifier': 'user-model',
})
})

it('carries session.previous_id when a new session replaces a stale one', async () => {
const now = Date.now()
localStorage.setItem(
SESSION_STORAGE_KEY,
JSON.stringify({
sessionId: 'stale-session-id',
startTime: now - SESSION_RESUME_THRESHOLD_MS * 2,
lastActivityTime: now - SESSION_RESUME_THRESHOLD_MS - 1_000,
reloadCount: 0,
}),
)
try {
const attributes = await buildInitializedResource()

expect(attributes['session.previous_id']).toBe('stale-session-id')
expect(attributes['session.id']).not.toBe('stale-session-id')
} finally {
localStorage.clear()
}
})

it('omits session.previous_id when there was no previous session', async () => {
localStorage.clear()
const attributes = await buildInitializedResource()

expect(attributes).not.toHaveProperty('session.previous_id')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,80 @@ describe('SessionManager session preservation', () => {
expect(mgr.getResumeInfo().reloadCount).toBe(0)
})

it('links a session started after a stale one via session.previous_id', async () => {
const store = new MemoryStore()
const now = Date.now()
store.seed({
sessionId: 'stale-session-id',
startTime: now - SESSION_RESUME_THRESHOLD_MS * 2,
lastActivityTime: now - SESSION_RESUME_THRESHOLD_MS - 1_000,
reloadCount: 0,
})

const mgr = new SessionManager({}, store)
await mgr.initialize()

expect(mgr.getPreviousSessionId()).toBe('stale-session-id')
expect(mgr.getSessionAttributes()).toEqual({
'session.id': mgr.getSessionInfo().sessionId,
'session.start_time': mgr.getSessionInfo().startTime.toString(),
'session.previous_id': 'stale-session-id',
})
// The next load links to this session, not the one before it.
expect(store.read().sessionId).toBe(mgr.getSessionInfo().sessionId)
})

it('links a session started by a cold start via session.previous_id', async () => {
const store = new MemoryStore()
const now = Date.now()
store.seed({
sessionId: 'prev-session-id',
startTime: now - 60_000,
lastActivityTime: now - 1_000,
reloadCount: 0,
})

const mgr = new SessionManager(
{ getProcessStartTimeMs: () => now - 500 },
store,
)
await mgr.initialize()

expect(mgr.getSessionAttributes()['session.previous_id']).toBe(
'prev-session-id',
)
})

it('omits session.previous_id when the session is resumed', async () => {
const store = new MemoryStore()
const now = Date.now()
store.seed({
sessionId: 'prev-session-id',
startTime: now - 60_000,
lastActivityTime: now - 1_000,
reloadCount: 0,
})

const mgr = new SessionManager({}, store)
await mgr.initialize()

expect(mgr.getSessionInfo().sessionId).toBe('prev-session-id')
expect(mgr.getPreviousSessionId()).toBeUndefined()
expect(mgr.getSessionAttributes()).not.toHaveProperty(
'session.previous_id',
)
})

it('omits session.previous_id when nothing was persisted', async () => {
const mgr = new SessionManager({}, new MemoryStore())
await mgr.initialize()

expect(mgr.getPreviousSessionId()).toBeUndefined()
expect(mgr.getSessionAttributes()).not.toHaveProperty(
'session.previous_id',
)
})

it('does not resume without a persistent store', async () => {
const mgr = new SessionManager({}, noOpStore)
await mgr.initialize()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export class SessionManager {
reloadCount: 0,
}
private lastPersistAt = 0
// The persisted session this JS load replaced (stale or cold start), so
// the new session's telemetry can link back to it as `session.previous_id`.
private previousSessionId?: string

constructor(options?: Options, store?: SessionStore) {
this.options = {
Expand Down Expand Up @@ -110,6 +113,7 @@ export class SessionManager {
startTime: Date.now(),
}
this.resumeInfo = { reloaded: false, elapsedMs: 0, reloadCount: 0 }
this.previousSessionId = undefined
}
}

Expand Down Expand Up @@ -161,6 +165,9 @@ export class SessionManager {
// completes.
this.reloadCount = 0
this.resumeInfo = { reloaded: false, elapsedMs: 0, reloadCount: 0 }
if (previous && previous.sessionId !== this.sessionInfo.sessionId) {
this.previousSessionId = previous.sessionId
}

if (this.options.debug && isColdStart) {
console.log(
Expand Down Expand Up @@ -271,10 +278,22 @@ export class SessionManager {
return { ...this.sessionInfo }
}

/**
* The ID of the persisted session this JS load replaced with a new one
* (the previous session timed out, or the app cold started). Undefined
* when the session was resumed or nothing was persisted.
*/
public getPreviousSessionId(): string | undefined {
return this.previousSessionId
}

public getSessionAttributes(): Record<string, string> {
return {
'session.id': this.sessionInfo.sessionId,
'session.start_time': this.sessionInfo.startTime.toString(),
...(this.previousSessionId
? { 'session.previous_id': this.previousSessionId }
: {}),
}
}

Expand Down
Loading