Skip to content

Commit 7d23f79

Browse files
feat: session replay reads OTel session.id (HDX-5736) (#3438)
1 parent e88f716 commit 7d23f79

19 files changed

Lines changed: 885 additions & 103 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperdx/app': patch
3+
---
4+
5+
feat: List and play session replays from OpenTelemetry browser SDKs that send `session.id`

‎packages/app/src/DOMPlayer.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
createRrwebChunkAssembler,
3636
RRWebStreamRow,
3737
} from '@/utils/rrwebChunkAssembler';
38+
import type { SessionIdKey } from '@/utils/sessionId';
3839

3940
import { FieldExpressionGenerator } from './hooks/useFieldExpressionGenerator';
4041

@@ -101,7 +102,7 @@ const URLHoverCard = memo(({ url }: { url: string }) => {
101102
});
102103

103104
export default function DOMPlayer({
104-
config: { dateRange, serviceName, sessionId, sourceId },
105+
config: { dateRange, serviceName, sessionId, sessionIdKey, sourceId },
105106
focus,
106107
setPlayerTime,
107108
playerState,
@@ -119,6 +120,7 @@ export default function DOMPlayer({
119120
dateRange: [Date, Date];
120121
serviceName: string;
121122
sessionId: string;
123+
sessionIdKey?: SessionIdKey;
122124
sourceId: string;
123125
};
124126
focus: { ts: number; setBy: string } | undefined;
@@ -247,6 +249,7 @@ export default function DOMPlayer({
247249
{
248250
serviceName,
249251
sessionId,
252+
sessionIdKey,
250253
sourceId,
251254
startDate: dateRange[0],
252255
endDate: dateRange[1],

‎packages/app/src/Playbar.tsx‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { keepPreviousData } from '@tanstack/react-query';
55

66
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
77

8-
import { SessionRow, sessionRowSchema } from './utils/sessions';
8+
import {
9+
getSessionRowHttp,
10+
SessionRow,
11+
sessionRowSchema,
12+
} from './utils/sessions';
913
import type { PlaybarMarker } from './PlaybarSlider';
1014
import { PlaybarSlider } from './PlaybarSlider';
1115
import { getShortUrl } from './utils';
@@ -71,9 +75,7 @@ export default function Playbar({
7175

7276
const errorMessage = event['error.message'] ?? '';
7377

74-
const url = event['http.url'] ?? '';
75-
const statusCode = Number(event['http.status_code'] ?? '');
76-
const method = event['http.method'] ?? '';
78+
const { url, statusCode, label } = getSessionRowHttp(event);
7779
const shortUrl = getShortUrl(url);
7880

7981
const isNavigation =
@@ -96,7 +98,7 @@ export default function Playbar({
9698
description: isNavigation
9799
? `Navigated to ${shortLocationHref}`
98100
: url.length > 0
99-
? `${statusCode} ${method}${url.length > 0 ? ` ${shortUrl}` : ''}`
101+
? `${label} ${shortUrl}`
100102
: errorMessage != null && errorMessage.length > 0
101103
? errorMessage
102104
: spanName === 'intercom.onShow'

‎packages/app/src/SessionEventList.tsx‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import { useVirtualizer } from '@tanstack/react-virtual';
1616
import useRowWhere, { RowWhereResult } from '@/hooks/useRowWhere';
1717

1818
import { useQueriedChartConfig } from './hooks/useChartConfig';
19-
import { SessionRow, sessionRowSchema } from './utils/sessions';
19+
import {
20+
getSessionRowHttp,
21+
SessionRow,
22+
sessionRowSchema,
23+
} from './utils/sessions';
2024
import { useFormatTime } from './useFormatTime';
2125
import { formatmmss, getShortUrl } from './utils';
2226

@@ -135,9 +139,7 @@ export const SessionEventList = ({
135139
const startOffset = new Date(timestamp).getTime();
136140
const endOffset = new Date(startOffset).getTime() + durationInMs;
137141

138-
const url = event['http.url'] ?? '';
139-
const statusCode = Number(event['http.status_code'] ?? '');
140-
const method = event['http.method'] ?? '';
142+
const { url, method, statusCode, label } = getSessionRowHttp(event);
141143
const shortUrl = getShortUrl(url);
142144

143145
const isNetworkRequest =
@@ -186,7 +188,7 @@ export const SessionEventList = ({
186188
: spanName === 'console.warn'
187189
? 'console.warn'
188190
: url.length > 0
189-
? `${statusCode} ${method}`
191+
? label
190192
: spanName === 'intercom.onShow'
191193
? 'Intercom Chat Opened'
192194
: isCustomEvent

‎packages/app/src/SessionSubpanel.tsx‎

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {
3131
} from '@tabler/icons-react';
3232

3333
import { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
34+
import type { SessionIdKey } from '@/utils/sessionId';
35+
import { buildSessionEventFilters } from '@/utils/sessions';
3436

3537
import SearchWhereInput from './components/SearchInput/SearchWhereInput';
3638
import useFieldExpressionGenerator from './hooks/useFieldExpressionGenerator';
@@ -47,6 +49,7 @@ const MemoPlaybar = memo(Playbar);
4749
function useSessionChartConfigs({
4850
traceSource,
4951
rumSessionId,
52+
sessionIdKey,
5053
where,
5154
whereLanguage,
5255
start,
@@ -55,6 +58,7 @@ function useSessionChartConfigs({
5558
}: {
5659
traceSource: TTraceSource;
5760
rumSessionId: string;
61+
sessionIdKey?: SessionIdKey;
5862
where: string;
5963
whereLanguage?: SearchConditionLanguage;
6064
start: Date;
@@ -96,6 +100,15 @@ function useSessionChartConfigs({
96100
valueExpression: `${getTraceSourceFieldExpression(traceSource.eventAttributesExpression ?? 'SpanAttributes', 'http.url')}`,
97101
alias: 'http.url',
98102
},
103+
...['url.full', 'http.request.method', 'http.response.status_code'].map(
104+
key => ({
105+
valueExpression: getTraceSourceFieldExpression(
106+
traceSource.eventAttributesExpression ?? 'SpanAttributes',
107+
key,
108+
),
109+
alias: key,
110+
}),
111+
),
99112
{
100113
// Using toString here because Javascript does not have the precision to accurately represent this
101114
valueExpression: `toString(cityHash64(${traceSource.traceIdExpression}, ${traceSource.parentSpanIdExpression}, ${traceSource.spanIdExpression}))`,
@@ -140,43 +153,18 @@ function useSessionChartConfigs({
140153
];
141154
}, [traceSource, getTraceSourceFieldExpression]);
142155

143-
// Events shown in the highlighted tab
144-
const highlightedEventsFilter = useMemo(
145-
() => ({
146-
type: 'lucene' as const,
147-
condition: `${traceSource.resourceAttributesExpression}.rum.sessionId:"${rumSessionId}"
148-
AND (
149-
${traceSource.eventAttributesExpression}.http.status_code:>299
150-
OR ${traceSource.eventAttributesExpression}.component:"error"
151-
OR ${traceSource.spanNameExpression}:"routeChange"
152-
OR ${traceSource.spanNameExpression}:"documentLoad"
153-
OR ${traceSource.spanNameExpression}:"intercom.onShow"
154-
OR ScopeName:"custom-action"
155-
)`,
156-
}),
157-
[traceSource, rumSessionId],
156+
const eventFilters = useMemo(
157+
() =>
158+
buildSessionEventFilters({
159+
traceSource,
160+
sessionId: rumSessionId,
161+
sessionIdKey,
162+
}),
163+
[traceSource, rumSessionId, sessionIdKey],
158164
);
159165

160-
const allEventsFilter = useMemo(() => {
161-
if (!getTraceSourceFieldExpression) return undefined;
162-
return {
163-
type: 'lucene' as const,
164-
condition: `${traceSource.resourceAttributesExpression}.rum.sessionId:"${rumSessionId}"
165-
AND (
166-
${traceSource.eventAttributesExpression}.http.status_code:*
167-
OR ${traceSource.eventAttributesExpression}.component:"console"
168-
OR ${traceSource.eventAttributesExpression}.component:"error"
169-
OR ${traceSource.spanNameExpression}:"routeChange"
170-
OR ${traceSource.spanNameExpression}:"documentLoad"
171-
OR ${traceSource.spanNameExpression}:"intercom.onShow"
172-
OR ScopeName:"custom-action"
173-
)`,
174-
};
175-
}, [traceSource, rumSessionId, getTraceSourceFieldExpression]);
176-
177166
const eventsConfig = useMemo<ChartConfigWithOptDateRange | undefined>(() => {
178-
if (!getTraceSourceFieldExpression || !select || !allEventsFilter)
179-
return undefined;
167+
if (!getTraceSourceFieldExpression || !select) return undefined;
180168
return {
181169
select: select,
182170
from: traceSource.from,
@@ -193,9 +181,7 @@ function useSessionChartConfigs({
193181
limit: 4000,
194182
offset: 0,
195183
},
196-
filters: [
197-
tab === 'highlighted' ? highlightedEventsFilter : allEventsFilter,
198-
],
184+
filters: eventFilters[tab === 'highlighted' ? 'highlighted' : 'all'],
199185
};
200186
}, [
201187
select,
@@ -205,8 +191,7 @@ function useSessionChartConfigs({
205191
where,
206192
whereLanguage,
207193
tab,
208-
highlightedEventsFilter,
209-
allEventsFilter,
194+
eventFilters,
210195
getTraceSourceFieldExpression,
211196
]);
212197

@@ -242,7 +227,7 @@ export default function SessionSubpanel({
242227
}: {
243228
traceSource: TTraceSource;
244229
sessionSource: TSessionSource;
245-
session: { serviceName: string };
230+
session: { serviceName: string; sessionIdKey?: SessionIdKey };
246231
rumSessionId: string;
247232
start: Date;
248233
end: Date;
@@ -373,6 +358,7 @@ export default function SessionSubpanel({
373358
const { eventsConfig, aliasMap } = useSessionChartConfigs({
374359
traceSource,
375360
rumSessionId,
361+
sessionIdKey: session.sessionIdKey,
376362
where: searchedQuery,
377363
whereLanguage,
378364
start,
@@ -519,6 +505,7 @@ export default function SessionSubpanel({
519505
serviceName: session.serviceName,
520506
sourceId: sessionSource.id,
521507
sessionId: rumSessionId,
508+
sessionIdKey: session.sessionIdKey,
522509
dateRange: [start, end],
523510
}}
524511
playerSpeed={playerSpeed}

‎packages/app/src/__tests__/sessions.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,23 @@ describe('buildRRWebStreamChartConfig', () => {
7171
expect(config.dateRange).toEqual([new Date(1000), new Date(2000)]);
7272
expect(config.limit).toEqual({ limit: 1000000, offset: 0 });
7373
});
74+
75+
it('looks up OTel sessions by session.id', () => {
76+
const otelConfig = buildRRWebStreamChartConfig({
77+
source: config,
78+
serviceName: 'my-service',
79+
sessionId: 'session-1',
80+
sessionIdKey: 'session.id',
81+
startDate: new Date(1000),
82+
endDate: new Date(2000),
83+
limit: 1000000,
84+
offset: 0,
85+
getSessionSourceFieldExpression,
86+
});
87+
expect(otelConfig.where).toBe(
88+
'ServiceName:"my-service" AND LogAttributes.session.id:"session-1"',
89+
);
90+
});
7491
});
7592

7693
describe('fetchServiceScopeFilters', () => {

0 commit comments

Comments
 (0)