Skip to content

Commit

Permalink
Replay improvements: explicit email filter, empty replay message, emp…
Browse files Browse the repository at this point in the history
…ty replay search filter
  • Loading branch information
MikeShi42 committed Sep 19, 2023
1 parent 70d5aa9 commit 0a91e91
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 52 deletions.
72 changes: 49 additions & 23 deletions packages/api/src/clickhouse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from './propertyTypeMappingsModel';
import {
SQLSerializer,
SearchQueryBuilder,
buildSearchColumnName,
buildSearchColumnName_OLD,
buildSearchQueryWhereCondition,
Expand Down Expand Up @@ -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')),
Expand All @@ -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(
Expand All @@ -1104,7 +1130,7 @@ export const getSessions = async ({
const result = await rows.json<ResponseJSON<Record<string, unknown>>>();
logger.info({
message: 'getSessions',
query,
query: executedQuery,
teamId,
took: Date.now() - ts,
});
Expand Down
12 changes: 7 additions & 5 deletions packages/api/src/clickhouse/searchQueryParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/DOMPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,12 @@ export default function DOMPlayer({
</div>
</div>
)}
{isReplayFullyLoaded && replayer.current == null && (
<div className="d-flex align-items-center justify-content-center bg-hdx-dark p-4 text-center text-muted">
No replay available for this session, most likely due to this session
starting and ending in a background tab.
</div>
)}
<div
ref={wrapper}
className={`player-wrapper overflow-hidden ${
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Form } from 'react-bootstrap';

export default function Dropdown<T extends string | number>({
name,
className,
disabled,
onChange,
options,
style,
value,
}: {
name?: string;
className?: string;
disabled?: boolean;
onChange: (value: T) => any;
Expand All @@ -17,6 +19,7 @@ export default function Dropdown<T extends string | number>({
}) {
return (
<Form.Select
name={name}
disabled={disabled}
role="button"
className={`shadow-none fw-bold ${
Expand Down
153 changes: 129 additions & 24 deletions packages/app/src/SessionsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Head from 'next/head';
import { Button } from 'react-bootstrap';
import { Button, Form } from 'react-bootstrap';
import {
useQueryParam,
StringParam,
Expand All @@ -8,10 +8,12 @@ import {
} from 'use-query-params';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { NumberParam } from 'serialize-query-params';
import { toast } from 'react-toastify';

import api from './api';
import SearchTimeRangePicker from './SearchTimeRangePicker';
import AppNav from './AppNav';
import Dropdown from './Dropdown';
import {
formatDistanceToNowStrictShort,
formatHumanReadableDate,
Expand Down Expand Up @@ -327,6 +329,8 @@ export default function SessionsPage() {
)}`;
}, []);

const [isEmailFilterExpanded, setIsEmailFilterExpanded] = useState(true);

return (
<div className="SessionsPage d-flex" style={{ height: '100vh' }}>
<Head>
Expand Down Expand Up @@ -382,30 +386,131 @@ export default function SessionsPage() {
</form>
</div>
</div>
<form
onSubmit={e => {
e.preventDefault();
setSearchedQuery(inputQuery);
}}
>
<SearchInput
inputRef={inputRef}
value={inputQuery}
onChange={value => setInputQuery(value)}
onSearch={() => {}}
placeholder="Search for a session by email, id..."
/>
<button
type="submit"
style={{
width: 0,
height: 0,
border: 0,
padding: 0,
<div className="d-flex align-items-center">
<div className="d-flex align-items-center me-2">
<span
className="rounded fs-8 text-nowrap border border-dark p-2"
style={{
borderTopRightRadius: '0 !important',
borderBottomRightRadius: '0 !important',
}}
title="Filters"
>
<i className="bi bi-funnel"></i>
</span>{' '}
<div className="d-flex align-items-center w-100 flex-grow-1">
<Button
variant="dark"
type="button"
className="text-muted-hover d-flex align-items-center fs-8 p-2"
onClick={() => setIsEmailFilterExpanded(v => !v)}
style={
isEmailFilterExpanded
? {
borderRadius: 0,
}
: {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}
}
>
Email
</Button>
{isEmailFilterExpanded && (
<form
className="d-flex"
onSubmit={e => {
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 = '';
}}
>
<Dropdown
name="op"
className="border border-dark fw-normal fs-8 p-2"
style={{ borderRadius: 0, minWidth: 100 }}
options={[
{
value: 'contains',
text: 'contains',
},
{ value: 'is', text: 'is' },
{ value: 'is_not', text: 'is not' },
]}
value={undefined}
onChange={() => {}}
/>
<Form.Control
type="text"
id="value"
name="value"
className="fs-8 p-2 w-100"
style={{ borderRadius: 0 }}
placeholder="value"
/>
<Button
type="submit"
variant="dark"
className="text-muted-hover d-flex align-items-center fs-8 p-2"
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}}
>
Add
</Button>
</form>
)}
</div>
</div>
<form
className="d-flex align-items-center flex-grow-1"
onSubmit={e => {
e.preventDefault();
setSearchedQuery(inputQuery);
}}
/>
</form>
<div style={{ minHeight: 0 }}>
>
<SearchInput
inputRef={inputRef}
value={inputQuery}
onChange={value => setInputQuery(value)}
onSearch={() => {}}
placeholder="Search for a session by email, id..."
/>
<button
type="submit"
style={{
width: 0,
height: 0,
border: 0,
padding: 0,
}}
/>
</form>
</div>
<div style={{ minHeight: 0 }} className="mt-4">
<SessionCardList
onClick={(sessionId, dateRange) => {
setSelectedSession({ id: sessionId, dateRange });
Expand Down

0 comments on commit 0a91e91

Please sign in to comment.