Skip to content

Commit

Permalink
[RUMF-1530] ✨ [RUMF-1530] send metadata as JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
BenoitZugmeyer committed Mar 30, 2023
1 parent 50668f5 commit 6432c4a
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 67 deletions.
38 changes: 22 additions & 16 deletions packages/rum/src/boot/startRecording.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,22 @@ describe('startRecording', () => {
const requests = await readSentRequests(1)
expect(requests[0].segment).toEqual(jasmine.any(Object))
expect(requests[0].metadata).toEqual({
'application.id': 'appId',
application: {
id: 'appId',
},
creation_reason: 'init',
end: jasmine.stringMatching(/^\d{13}$/),
has_full_snapshot: 'true',
records_count: String(recordsPerFullSnapshot()),
'session.id': 'session-id',
start: jasmine.stringMatching(/^\d{13}$/),
raw_segment_size: jasmine.stringMatching(/^\d+$/),
'view.id': 'view-id',
index_in_view: '0',
has_full_snapshot: true,
records_count: recordsPerFullSnapshot(),
session: {
id: 'session-id',
},
start: jasmine.any(Number),
raw_segment_size: jasmine.any(Number),
view: {
id: 'view-id',
},
index_in_view: 0,
source: 'browser',
})
})
Expand All @@ -105,7 +111,7 @@ describe('startRecording', () => {
}

const requests = await readSentRequests(1)
expect(requests[0].metadata.records_count).toBe(String(inputCount + recordsPerFullSnapshot()))
expect(requests[0].metadata.records_count).toBe(inputCount + recordsPerFullSnapshot())
})

it('stops sending new segment when the session is expired', async () => {
Expand All @@ -120,7 +126,7 @@ describe('startRecording', () => {
flushSegment(lifeCycle)

const requests = await readSentRequests(1)
expect(requests[0].metadata.records_count).toBe(String(1 + recordsPerFullSnapshot()))
expect(requests[0].metadata.records_count).toBe(1 + recordsPerFullSnapshot())
})

it('restarts sending segments when the session is renewed', async () => {
Expand All @@ -136,8 +142,8 @@ describe('startRecording', () => {
flushSegment(lifeCycle)

const requests = await readSentRequests(1)
expect(requests[0].metadata.records_count).toBe('1')
expect(requests[0].metadata['session.id']).toBe('new-session-id')
expect(requests[0].metadata.records_count).toBe(1)
expect(requests[0].metadata.session.id).toBe('new-session-id')
})

it('takes a full snapshot when the view changes', async () => {
Expand All @@ -147,7 +153,7 @@ describe('startRecording', () => {
flushSegment(lifeCycle)

const requests = await readSentRequests(2)
expect(requests[1].metadata.has_full_snapshot).toBe('true')
expect(requests[1].metadata.has_full_snapshot).toBe(true)
})

it('full snapshot related records should have the view change date', async () => {
Expand Down Expand Up @@ -177,7 +183,7 @@ describe('startRecording', () => {
flushSegment(lifeCycle)

const requests = await readSentRequests(2)
expect(requests[0].metadata['view.id']).toBe('view-id')
expect(requests[0].metadata.view.id).toBe('view-id')
const records = requests[0].segment.records
expect(records[records.length - 1].type).toBe(RecordType.ViewEnd)
})
Expand Down Expand Up @@ -218,7 +224,7 @@ describe('startRecording', () => {
flushSegment(lifeCycle)

const requests = await readSentRequests(1)
expect(requests[0].metadata.records_count).toBe(String(1 + recordsPerFullSnapshot()))
expect(requests[0].metadata.records_count).toBe(1 + recordsPerFullSnapshot())
})

it('stops taking full snapshots on view creation', async () => {
Expand All @@ -229,7 +235,7 @@ describe('startRecording', () => {
flushSegment(lifeCycle)

const requests = await readSentRequests(1)
expect(requests[0].metadata.records_count).toBe(String(recordsPerFullSnapshot()))
expect(requests[0].metadata.records_count).toBe(recordsPerFullSnapshot())
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,49 @@
import { toFormEntries } from './buildReplayPayload'
import pako from 'pako'
import type { BrowserSegment, BrowserSegmentMetadata } from '../../types'
import { readReplayPayload } from '../../../test'
import { buildReplayPayload } from './buildReplayPayload'

describe('toFormEntries', () => {
let callbackSpy: jasmine.Spy<(key: string, value: string) => void>
describe('buildReplayPayload', () => {
const SEGMENT = { foo: 'bar' } as unknown as BrowserSegment
const SERIALIZED_SEGMENT = JSON.stringify(SEGMENT)
const COMPRESSED_SEGMENT = pako.deflate(SERIALIZED_SEGMENT)
const METADATA: BrowserSegmentMetadata = {
application: { id: 'xxx' },
session: { id: 'xxx' },
view: { id: 'xxx' },
start: 1,
end: 2,
records_count: 10,
source: 'browser',
creation_reason: 'init',
}
const METADATA_AND_SEGMENT_SIZES = {
...METADATA,
raw_segment_size: SERIALIZED_SEGMENT.length,
}

beforeEach(() => {
callbackSpy = jasmine.createSpy()
it('adds the segment as a file', async () => {
const payload = buildReplayPayload(COMPRESSED_SEGMENT, METADATA, SERIALIZED_SEGMENT.length)
const segmentEntry = (payload.data as FormData).get('segment')! as File
expect(segmentEntry.size).toBe(COMPRESSED_SEGMENT.byteLength)
expect(segmentEntry.name).toBe('xxx-1')
expect(segmentEntry.type).toBe('application/octet-stream')
const { segment } = await readReplayPayload(payload)
expect(segment).toEqual(SEGMENT)
})

it('handles top level properties', () => {
toFormEntries({ foo: 'bar', zig: 'zag' }, callbackSpy)
expect(callbackSpy.calls.allArgs()).toEqual([
['foo', 'bar'],
['zig', 'zag'],
])
it('adds the metadata plus the segment sizes as the `event` entry', async () => {
const payload = buildReplayPayload(COMPRESSED_SEGMENT, METADATA, SERIALIZED_SEGMENT.length)
const eventEntry = (payload.data as FormData).get('event')! as File
expect(eventEntry.size).toBe(JSON.stringify(METADATA_AND_SEGMENT_SIZES).length)
expect(eventEntry.name).toBe('blob')
expect(eventEntry.type).toBe('application/json')
const { metadata } = await readReplayPayload(payload)
expect(metadata).toEqual(METADATA_AND_SEGMENT_SIZES)
})

it('handles nested properties', () => {
toFormEntries({ foo: { bar: 'baz', zig: { zag: 'zug' } } }, callbackSpy)
expect(callbackSpy.calls.allArgs()).toEqual([
['foo.bar', 'baz'],
['foo.zig.zag', 'zug'],
])
})

it('converts values to string', () => {
toFormEntries({ foo: 42, bar: null }, callbackSpy)
expect(callbackSpy.calls.allArgs()).toEqual([
['foo', '42'],
['bar', 'null'],
])
it('returns the approximative byte counts of the request', () => {
const payload = buildReplayPayload(COMPRESSED_SEGMENT, METADATA, SERIALIZED_SEGMENT.length)
expect(payload.bytesCount).toBe(COMPRESSED_SEGMENT.byteLength)
})
})
26 changes: 13 additions & 13 deletions packages/rum/src/domain/segmentCollection/buildReplayPayload.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { Payload } from '@datadog/browser-core'
import { objectEntries } from '@datadog/browser-core'
import { assign } from '@datadog/browser-core'
import type { BrowserSegmentMetadata } from '../../types'

export type BrowserSegmentMetadataAndSegmentSizes = BrowserSegmentMetadata & {
raw_segment_size: number
}

export function buildReplayPayload(
data: Uint8Array,
metadata: BrowserSegmentMetadata,
Expand All @@ -17,18 +21,14 @@ export function buildReplayPayload(
`${metadata.session.id}-${metadata.start}`
)

toFormEntries(metadata, (key, value) => formData.append(key, value))
formData.append('raw_segment_size', rawSegmentBytesCount.toString())
const metadataAndSegmentSizes: BrowserSegmentMetadataAndSegmentSizes = assign(
{
raw_segment_size: rawSegmentBytesCount,
},
metadata
)
const serializedMetadataAndSegmentSizes = JSON.stringify(metadataAndSegmentSizes)
formData.append('event', new Blob([serializedMetadataAndSegmentSizes], { type: 'application/json' }))

return { data: formData, bytesCount: data.byteLength }
}

export function toFormEntries(input: object, onEntry: (key: string, value: string) => void, prefix = '') {
objectEntries(input as { [key: string]: unknown }).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null) {
toFormEntries(value, onEntry, `${prefix}${key}.`)
} else {
onEntry(`${prefix}${key}`, String(value))
}
})
}
1 change: 1 addition & 0 deletions packages/rum/src/domain/segmentCollection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { startSegmentCollection, setSegmentBytesLimit } from './segmentCollectio
export { DeflateWorker, DeflateWorkerAction, DeflateWorkerListener } from './deflateWorker'
export { startDeflateWorker } from './startDeflateWorker'
export { SEGMENT_BYTES_LIMIT } from './segmentCollection'
export type { BrowserSegmentMetadataAndSegmentSizes } from './buildReplayPayload'
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ describe('startSegmentCollection', () => {
worker.processAllMessages()

expect(httpRequestSpy.send).toHaveBeenCalledTimes(1)
expect((await readMostRecentMetadata(httpRequestSpy.send)).records_count).toBe('4')
expect((await readMostRecentMetadata(httpRequestSpy.send)).records_count).toBe(4)
})

it('does not flush segment prematurely when records from the previous segment are still being processed', async () => {
Expand All @@ -229,7 +229,7 @@ describe('startSegmentCollection', () => {
worker.processAllMessages()

expect(httpRequestSpy.send).toHaveBeenCalledTimes(1)
expect((await readMostRecentMetadata(httpRequestSpy.send)).records_count).toBe('2')
expect((await readMostRecentMetadata(httpRequestSpy.send)).records_count).toBe(2)
})
})

Expand Down
13 changes: 2 additions & 11 deletions packages/rum/test/readReplayPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import pako from 'pako'

import type { Payload } from '@datadog/browser-core'
import type { BrowserSegment } from '../src/types'
import type { BrowserSegmentMetadataAndSegmentSizes } from '../src/domain/segmentCollection'

export async function readReplayPayload(payload: Payload) {
return {
Expand All @@ -16,18 +17,8 @@ function readSegmentFromReplayPayload(payload: Payload) {
}) as Promise<BrowserSegment>
}

// In the next commit, this method will change and will be async. This is an intermediary
// implementation to prepare for the real metadata change.
// eslint-disable-next-line @typescript-eslint/require-await
export async function readMetadataFromReplayPayload(payload: Payload) {
const data = payload.data as FormData
const result: Record<string, string> = {}
data.forEach((value, key) => {
if (typeof value === 'string') {
result[key] = value
}
})
return result
return readJsonBlob((payload.data as FormData).get('event') as Blob) as Promise<BrowserSegmentMetadataAndSegmentSizes>
}

function readJsonBlob(blob: Blob, { decompress = false }: { decompress?: boolean } = {}) {
Expand Down

0 comments on commit 6432c4a

Please sign in to comment.