-
Notifications
You must be signed in to change notification settings - Fork 120
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: add an explicit in-memory buffer #1217
Changes from all commits
77bc9e0
13b861e
3748f3e
9a3035a
b1fc393
90d0f41
68b869b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -92,8 +92,34 @@ type SessionRecordingStatus = 'disabled' | 'sampled' | 'active' | 'buffering' | |
interface SnapshotBuffer { | ||
size: number | ||
data: any[] | ||
sessionId: string | null | ||
windowId: string | null | ||
sessionId: string | ||
windowId: string | ||
|
||
readonly mostRecentSnapshotTimestamp: number | null | ||
add(properties: Properties): void | ||
} | ||
|
||
class InMemoryBuffer implements SnapshotBuffer { | ||
size: number | ||
data: any[] | ||
sessionId: string | ||
windowId: string | ||
|
||
get mostRecentSnapshotTimestamp(): number | null { | ||
return this.data.length ? this.data[this.data.length - 1].timestamp : null | ||
} | ||
|
||
constructor(sessionId: string, windowId: string) { | ||
this.size = 0 | ||
this.data = [] | ||
this.sessionId = sessionId | ||
this.windowId = windowId | ||
} | ||
|
||
add(properties: Properties) { | ||
this.size += properties.$snapshot_bytes | ||
this.data.push(properties.$snapshot_data) | ||
} | ||
} | ||
|
||
interface QueuedRRWebEvent { | ||
|
@@ -112,12 +138,11 @@ const newQueuedEvent = (rrwebMethod: () => void): QueuedRRWebEvent => ({ | |
const LOGGER_PREFIX = '[SessionRecording]' | ||
|
||
export class SessionRecording { | ||
private instance: PostHog | ||
private _endpoint: string | ||
private flushBufferTimer?: any | ||
|
||
// we have a buffer - that contains PostHog snapshot events ready to be sent to the server | ||
private buffer?: SnapshotBuffer | ||
private buffer: SnapshotBuffer | ||
// and a queue - that contains rrweb events that we want to send to rrweb, but rrweb wasn't able to accept them yet | ||
private queuedRRWebEvents: QueuedRRWebEvent[] = [] | ||
|
||
|
@@ -153,7 +178,6 @@ export class SessionRecording { | |
|
||
private get sessionManager() { | ||
if (!this.instance.sessionManager) { | ||
logger.error(LOGGER_PREFIX + ' started without valid sessionManager') | ||
throw new Error(LOGGER_PREFIX + ' started without valid sessionManager. This is a bug.') | ||
} | ||
|
||
|
@@ -166,9 +190,9 @@ export class SessionRecording { | |
} | ||
|
||
private get sessionDuration(): number | null { | ||
const mostRecentSnapshot = this.buffer?.data[this.buffer?.data.length - 1] | ||
const mostRecentSnapshotTimestamp = this.buffer.mostRecentSnapshotTimestamp | ||
const { sessionStartTimestamp } = this.sessionManager.checkAndGetSessionAndWindowId(true) | ||
return mostRecentSnapshot ? mostRecentSnapshot.timestamp - sessionStartTimestamp : null | ||
return mostRecentSnapshotTimestamp ? mostRecentSnapshotTimestamp - sessionStartTimestamp : null | ||
} | ||
|
||
private get isRecordingEnabled() { | ||
|
@@ -250,8 +274,7 @@ export class SessionRecording { | |
} | ||
} | ||
|
||
constructor(instance: PostHog) { | ||
this.instance = instance | ||
constructor(private readonly instance: PostHog) { | ||
this._captureStarted = false | ||
this._endpoint = BASE_ENDPOINT | ||
this.stopRrweb = undefined | ||
|
@@ -286,7 +309,7 @@ export class SessionRecording { | |
this.sessionId = sessionId | ||
this.windowId = windowId | ||
|
||
this.buffer = this.clearBuffer() | ||
this.buffer = new InMemoryBuffer(this.sessionId, this.windowId) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. explicitly assigning it so that it doesn't need to be undefinable |
||
|
||
// on reload there might be an already sampled session that should be continued before decide response, | ||
// so we call this here _and_ in the decide response | ||
|
@@ -846,24 +869,11 @@ export class SessionRecording { | |
return url | ||
} | ||
|
||
private clearBuffer(): SnapshotBuffer { | ||
this.buffer = undefined | ||
|
||
return { | ||
size: 0, | ||
data: [], | ||
sessionId: this.sessionId, | ||
windowId: this.windowId, | ||
} | ||
private clearBuffer(): void { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but keeping this convenience method for clarity |
||
this.buffer = new InMemoryBuffer(this.sessionId, this.windowId) | ||
} | ||
|
||
// the intention is a buffer that (currently) is used only after a decide response enables session recording | ||
// it is called ever X seconds using the flushBufferTimer so that we don't have to wait for the buffer to fill up | ||
// when it is called on a timer it assumes that it can definitely flush | ||
// it is flushed when the session id changes or the size of the buffered data gets too great (1mb by default) | ||
// first change: if the recording is in buffering mode, | ||
// flush buffer simply resets the timer and returns the existing flush buffer | ||
private _flushBuffer() { | ||
private _flushBuffer(): void { | ||
if (this.flushBufferTimer) { | ||
clearTimeout(this.flushBufferTimer) | ||
this.flushBufferTimer = undefined | ||
|
@@ -881,42 +891,31 @@ export class SessionRecording { | |
this.flushBufferTimer = setTimeout(() => { | ||
this._flushBuffer() | ||
}, RECORDING_BUFFER_TIMEOUT) | ||
return this.buffer || this.clearBuffer() | ||
|
||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
if (this.buffer && this.buffer.data.length !== 0) { | ||
if (this.buffer.data.length > 0) { | ||
this._captureSnapshot({ | ||
$snapshot_bytes: this.buffer.size, | ||
$snapshot_data: this.buffer.data, | ||
$session_id: this.buffer.sessionId, | ||
$window_id: this.buffer.windowId, | ||
}) | ||
|
||
return this.clearBuffer() | ||
} else { | ||
return this.buffer || this.clearBuffer() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
this.clearBuffer() | ||
} | ||
|
||
private _captureSnapshotBuffered(properties: Properties) { | ||
const additionalBytes = 2 + (this.buffer?.data.length || 0) // 2 bytes for the array brackets and 1 byte for each comma | ||
if ( | ||
!this.buffer || | ||
this.buffer.size + properties.$snapshot_bytes + additionalBytes > RECORDING_MAX_EVENT_SIZE || | ||
(!!this.buffer.sessionId && this.buffer.sessionId !== this.sessionId) | ||
this.buffer.sessionId !== this.sessionId | ||
) { | ||
this.buffer = this._flushBuffer() | ||
} | ||
|
||
if (isNull(this.buffer.sessionId) && !isNull(this.sessionId)) { | ||
// session id starts null but has now been assigned, update the buffer | ||
this.buffer.sessionId = this.sessionId | ||
this.buffer.windowId = this.windowId | ||
Comment on lines
-911
to
-914
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. buffer always has a session id now |
||
this._flushBuffer() | ||
} | ||
|
||
this.buffer.size += properties.$snapshot_bytes | ||
this.buffer.data.push(properties.$snapshot_data) | ||
|
||
this.buffer.add(properties) | ||
if (!this.flushBufferTimer) { | ||
this.flushBufferTimer = setTimeout(() => { | ||
this._flushBuffer() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no benefit to this being allowed to be undefined