diff --git a/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--dark.png b/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--dark.png index 915d1d37460d0..f1e9cc5f96fea 100644 Binary files a/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--dark.png and b/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--light.png b/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--light.png index 33b26bef7b64c..0370f8bb53c4f 100644 Binary files a/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--light.png and b/frontend/__snapshots__/scenes-app-data-management--ingestion-warnings--light.png differ diff --git a/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx b/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx index 7c2e1c57cfdad..acfbda51301c8 100644 --- a/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx +++ b/frontend/src/scenes/data-management/ingestion-warnings/IngestionWarningsView.tsx @@ -23,6 +23,7 @@ const WARNING_TYPE_TO_DESCRIPTION = { message_size_too_large: 'Discarded event exceeding 1MB limit', replay_timestamp_invalid: 'Replay event timestamp is invalid', replay_timestamp_too_far: 'Replay event timestamp was too far in the future', + replay_message_too_large: 'Replay data was dropped because it was too large to ingest', } const WARNING_TYPE_RENDERER = { @@ -200,6 +201,34 @@ const WARNING_TYPE_RENDERER = { ) }, + replay_message_too_large: function Render(warning: IngestionWarning): JSX.Element { + const details: { + timestamp: string + session_id: string + } = { + timestamp: warning.details.timestamp, + session_id: warning.details.replayRecord.session_id, + } + return ( + <> + Session replay data dropped due to its size, this can cause playback problems: + +
+ } + data-attr="message-too-large-view-recording" + > + View recording + +
+ + ) + }, } export function IngestionWarningsView(): JSX.Element { diff --git a/frontend/src/scenes/data-management/ingestion-warnings/__mocks__/ingestion-warnings-response.ts b/frontend/src/scenes/data-management/ingestion-warnings/__mocks__/ingestion-warnings-response.ts index 473f001793616..4d37b19e3284b 100644 --- a/frontend/src/scenes/data-management/ingestion-warnings/__mocks__/ingestion-warnings-response.ts +++ b/frontend/src/scenes/data-management/ingestion-warnings/__mocks__/ingestion-warnings-response.ts @@ -3,6 +3,22 @@ import { dayjs } from 'lib/dayjs' export const ingestionWarningsResponse = (baseTime: dayjs.Dayjs): { results: Record } => { return { results: [ + { + type: 'replay_message_too_large', + lastSeen: baseTime.subtract(1, 'day'), + sparkline: [[1, baseTime.format('YYYY-MM-DD')]], + warnings: [ + { + type: 'replay_message_too_large', + timestamp: baseTime.subtract(1, 'day'), + details: { + timestamp: 'not a date', + replayRecord: { session_id: 'some uuid' }, + }, + }, + ], + count: 1, + }, { type: 'replay_timestamp_invalid', lastSeen: baseTime.subtract(1, 'day'), diff --git a/plugin-server/src/main/ingestion-queues/session-recording/process-event.ts b/plugin-server/src/main/ingestion-queues/session-recording/process-event.ts index a729fb23fcff6..81cc72f3c3eb9 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/process-event.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/process-event.ts @@ -255,7 +255,7 @@ export const createSessionReplayEvent = ( session_id: string, events: RRWebEvent[], snapshot_source: string | null -) => { +): { event: SummarizedSessionRecordingEvent; warnings: string[] } => { const timestamps = getTimestampsFrom(events) // but every event where chunk index = 0 must have an eventsSummary @@ -268,6 +268,8 @@ export const createSessionReplayEvent = ( throw new Error('ignoring an empty session recording event') } + const warnings: string[] = [] + let clickCount = 0 let keypressCount = 0 let mouseActivity = 0 @@ -303,6 +305,10 @@ export const createSessionReplayEvent = ( consoleErrorCount += 1 } } + + if (event.type === RRWebEventType.Custom && event.data?.tag === 'Message too large') { + warnings.push('replay_message_too_large') + } }) const activeTime = activeMilliseconds(events) @@ -330,5 +336,5 @@ export const createSessionReplayEvent = ( snapshot_source: snapshot_source || 'web', } - return data + return { event: data, warnings } } diff --git a/plugin-server/src/main/ingestion-queues/session-recording/services/replay-events-ingester.ts b/plugin-server/src/main/ingestion-queues/session-recording/services/replay-events-ingester.ts index 669a8edc72a90..c97539b796c14 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/services/replay-events-ingester.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/services/replay-events-ingester.ts @@ -115,7 +115,7 @@ export class ReplayEventsIngester { try { const rrwebEvents = Object.values(event.eventsByWindowId).reduce((acc, val) => acc.concat(val), []) - const replayRecord = createSessionReplayEvent( + const { event: replayRecord, warnings } = createSessionReplayEvent( randomUUID(), event.team_id, event.distinct_id, @@ -145,6 +145,22 @@ export class ReplayEventsIngester { return drop('invalid_timestamp') } } + + await Promise.allSettled( + warnings.map(async (warning) => { + await captureIngestionWarning( + new KafkaProducerWrapper(this.producer), + event.team_id, + warning, + { + replayRecord, + timestamp: replayRecord.first_timestamp, + processingTimestamp: DateTime.now().toISO(), + }, + { key: event.session_id } + ) + }) + ) } catch (e) { captureException(e, { extra: { diff --git a/plugin-server/tests/main/ingestion-queues/session-recording/process-event.test.ts b/plugin-server/tests/main/ingestion-queues/session-recording/process-event.test.ts index d74d3a2de9e23..23c89d7c64555 100644 --- a/plugin-server/tests/main/ingestion-queues/session-recording/process-event.test.ts +++ b/plugin-server/tests/main/ingestion-queues/session-recording/process-event.test.ts @@ -33,6 +33,7 @@ describe('session recording process event', () => { | 'message_count' | 'snapshot_source' > + expectedWarnings: string[] }[] = [ { testDescription: 'click and mouse counts are detected', @@ -68,6 +69,7 @@ describe('session recording process event', () => { message_count: 1, snapshot_source: 'web', }, + expectedWarnings: [], }, { testDescription: 'keyboard press is detected', @@ -91,6 +93,7 @@ describe('session recording process event', () => { message_count: 1, snapshot_source: 'web', }, + expectedWarnings: [], }, { testDescription: 'console log entries are counted', @@ -179,6 +182,7 @@ describe('session recording process event', () => { message_count: 1, snapshot_source: 'web', }, + expectedWarnings: [], }, { testDescription: 'url can be detected in meta event', @@ -216,6 +220,7 @@ describe('session recording process event', () => { message_count: 1, snapshot_source: 'web', }, + expectedWarnings: [], }, { testDescription: 'first url detection takes the first url whether meta url or payload url', @@ -257,6 +262,7 @@ describe('session recording process event', () => { message_count: 1, snapshot_source: 'web', }, + expectedWarnings: [], }, { testDescription: 'first url detection can use payload url', @@ -302,6 +308,7 @@ describe('session recording process event', () => { message_count: 1, snapshot_source: 'web', }, + expectedWarnings: [], }, { testDescription: 'negative timestamps are not included when picking timestamps', @@ -330,6 +337,7 @@ describe('session recording process event', () => { message_count: 1, snapshot_source: 'web', }, + expectedWarnings: [], }, { testDescription: 'overlapping windows are summed separately for activity', @@ -361,6 +369,7 @@ describe('session recording process event', () => { message_count: 1, snapshot_source: 'web', }, + expectedWarnings: [], }, { testDescription: 'mobile snapshot source is stored', @@ -384,13 +393,41 @@ describe('session recording process event', () => { size: 82, snapshot_source: 'mobile', }, + expectedWarnings: [], + }, + { + testDescription: 'message too large warning is reported', + snapshotData: { + events_summary: [ + { timestamp: 1682449093000, type: 3, data: { source: 2, type: 2 }, windowId: '1' }, + { timestamp: 1682449093000, type: 5, data: { tag: 'Message too large' }, windowId: '1' }, + ], + }, + snapshotSource: 'web', + expected: { + active_milliseconds: 1, + click_count: 1, + console_error_count: 0, + console_log_count: 0, + console_warn_count: 0, + event_count: 2, + first_timestamp: '2023-04-25 18:58:13.000', + first_url: null, + keypress_count: 0, + last_timestamp: '2023-04-25 18:58:13.000', + message_count: 1, + mouse_activity_count: 1, + size: 169, + snapshot_source: 'web', + }, + expectedWarnings: ['replay_message_too_large'], }, ] it.each(sessionReplayEventTestCases)( 'session replay event generation - $testDescription', - ({ snapshotData, snapshotSource, expected }) => { - const data = createSessionReplayEvent( + ({ snapshotData, snapshotSource, expected, expectedWarnings }) => { + const { event: data, warnings } = createSessionReplayEvent( 'some-id', 12345, '5AzhubH8uMghFHxXq0phfs14JOjH6SA2Ftr1dzXj7U4', @@ -399,6 +436,8 @@ describe('session recording process event', () => { snapshotSource || null ) + expect(warnings).toStrictEqual(expectedWarnings) + const expectedEvent: SummarizedSessionRecordingEvent = { distinct_id: '5AzhubH8uMghFHxXq0phfs14JOjH6SA2Ftr1dzXj7U4', session_id: 'abcf-efg',