diff --git a/packages/api/src/clickhouse/index.ts b/packages/api/src/clickhouse/index.ts index 28f6a44ee..990240cda 100644 --- a/packages/api/src/clickhouse/index.ts +++ b/packages/api/src/clickhouse/index.ts @@ -23,6 +23,7 @@ import { } from './propertyTypeMappingsModel'; import { SQLSerializer, + SearchQueryBuilder, buildSearchColumnName, buildSearchColumnName_OLD, buildSearchQueryWhereCondition, @@ -1057,27 +1058,25 @@ export const getSessions = async ({ .map(props => buildCustomColumn(props[0], props[1])) .map(column => SqlString.raw(column)); - const query = SqlString.format( - ` - SELECT - MAX(timestamp) AS maxTimestamp, - MIN(timestamp) AS minTimestamp, - count() AS sessionCount, - countIf(?='user-interaction') AS interactionCount, - countIf(severity_text = 'error') AS errorCount, - ? AS sessionId, - ? - FROM ?? - WHERE ? AND (?) - GROUP BY sessionId - ${ - // If the user is giving us an explicit query, we don't need to filter out sessions with no interactions - // this is because the events that match the query might not be user interactions, and we'll just show 0 results otherwise. - q.length === 0 ? 'HAVING interactionCount > 0' : '' - } - ORDER BY maxTimestamp DESC - LIMIT ?, ? - `, + const sessionsWithSearchQuery = SqlString.format( + `SELECT + MAX(timestamp) AS maxTimestamp, + MIN(timestamp) AS minTimestamp, + count() AS sessionCount, + countIf(?='user-interaction') AS interactionCount, + countIf(severity_text = 'error') AS errorCount, + ? AS sessionId, + ? + FROM ?? + WHERE ? AND (?) + GROUP BY sessionId + ${ + // If the user is giving us an explicit query, we don't need to filter out sessions with no interactions + // this is because the events that match the query might not be user interactions, and we'll just show 0 results otherwise. + q.length === 0 ? 'HAVING interactionCount > 0' : '' + } + ORDER BY maxTimestamp DESC + LIMIT ?, ?`, [ SqlString.raw(buildSearchColumnName('string', 'component')), SqlString.raw(buildSearchColumnName('string', 'rum_session_id')), @@ -1090,9 +1089,36 @@ export const getSessions = async ({ ], ); + const sessionsWithRecordingsQuery = SqlString.format( + `WITH sessions AS (${sessionsWithSearchQuery}), +sessionIdsWithRecordings AS ( + SELECT DISTINCT _rum_session_id as sessionId + FROM ?? + WHERE span_name='record init' + AND (_rum_session_id IN (SELECT sessions.sessionId FROM sessions)) + AND (?) +) +SELECT * +FROM sessions +WHERE sessions.sessionId IN ( + SELECT sessionIdsWithRecordings.sessionId FROM sessionIdsWithRecordings + )`, + [ + tableName, + SqlString.raw(SearchQueryBuilder.timestampInBetween(startTime, endTime)), + ], + ); + + // If the user specifes a query, we need to filter out returned sessions + // by the 'record init' event being included so we don't return "blank" + // sessions, this can be optimized once we record background status + // of all events in the RUM package + const executedQuery = + q.length === 0 ? sessionsWithSearchQuery : sessionsWithRecordingsQuery; + const ts = Date.now(); const rows = await client.query({ - query, + query: executedQuery, format: 'JSON', clickhouse_settings: { additional_table_filters: buildLogStreamAdditionalFilters( @@ -1104,7 +1130,7 @@ export const getSessions = async ({ const result = await rows.json>>(); logger.info({ message: 'getSessions', - query, + query: executedQuery, teamId, took: Date.now() - ts, }); diff --git a/packages/api/src/clickhouse/searchQueryParser.ts b/packages/api/src/clickhouse/searchQueryParser.ts index 434795682..ec21a9f2a 100644 --- a/packages/api/src/clickhouse/searchQueryParser.ts +++ b/packages/api/src/clickhouse/searchQueryParser.ts @@ -588,13 +588,15 @@ export class SearchQueryBuilder { return this; } + static timestampInBetween(startTime: number, endTime: number) { + return `_timestamp_sort_key >= ${msToBigIntNs( + startTime, + )} AND _timestamp_sort_key < ${msToBigIntNs(endTime)}`; + } + // startTime and endTime are unix in ms timestampInBetween(startTime: number, endTime: number) { - this.and( - `_timestamp_sort_key >= ${msToBigIntNs( - startTime, - )} AND _timestamp_sort_key < ${msToBigIntNs(endTime)}`, - ); + this.and(SearchQueryBuilder.timestampInBetween(startTime, endTime)); return this; } diff --git a/packages/app/src/DOMPlayer.tsx b/packages/app/src/DOMPlayer.tsx index a13d40d12..97acc14e4 100644 --- a/packages/app/src/DOMPlayer.tsx +++ b/packages/app/src/DOMPlayer.tsx @@ -384,6 +384,12 @@ export default function DOMPlayer({ )} + {isReplayFullyLoaded && replayer.current == null && ( +
+ No replay available for this session, most likely due to this session + starting and ending in a background tab. +
+ )}
({ + name, className, disabled, onChange, @@ -8,6 +9,7 @@ export default function Dropdown({ style, value, }: { + name?: string; className?: string; disabled?: boolean; onChange: (value: T) => any; @@ -17,6 +19,7 @@ export default function Dropdown({ }) { return ( @@ -382,30 +386,131 @@ export default function SessionsPage() {
-
{ - e.preventDefault(); - setSearchedQuery(inputQuery); - }} - > - setInputQuery(value)} - onSearch={() => {}} - placeholder="Search for a session by email, id..." - /> - + {isEmailFilterExpanded && ( + { + e.preventDefault(); + + // TODO: Transition to react-hook-form or controlled state + // @ts-ignore + const value = e.target.value.value; + // @ts-ignore + const op = e.target.op.value; + + setSearchedQuery( + ( + inputQuery + + (op === 'is' + ? ` userEmail:"${value}"` + : op === 'is_not' + ? ` -userEmail:"${value}"` + : ` userEmail:${value}`) + ).trim(), + ); + + toast.success('Added filter to search query'); + inputRef.current?.focus(); + + // @ts-ignore + e.target.value.value = ''; + }} + > + {}} + /> + + + + )} + + +
{ + e.preventDefault(); + setSearchedQuery(inputQuery); }} - /> -
-
+ > + setInputQuery(value)} + onSearch={() => {}} + placeholder="Search for a session by email, id..." + /> +
+
{ setSelectedSession({ id: sessionId, dateRange });