123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- import {useEffect, useMemo, useState} from 'react';
- import first from 'lodash/first';
- import fetchReplayClicks from 'sentry/utils/replays/fetchReplayClicks';
- import {MutableSearch} from 'sentry/utils/tokenizeSearch';
- import useApi from 'sentry/utils/useApi';
- import {useLocation} from 'sentry/utils/useLocation';
- export type TimeOffsetLocationQueryParams = {
- /**
- * The time when the event happened.
- * Anything that can be parsed by `new Date()`; for example a timestamp in ms
- * or an ISO 8601 formatted string.
- */
- event_t?: string;
- /**
- * The query that was used on the index page. If it includes `click.*` fields
- * then we will use those to lookup a list of `offsetMs` values
- */
- query?: string;
- /**
- * A specific offset into the replay. Number of seconds.
- * Should be less than the duration of the replay
- */
- t?: string;
- };
- type Opts = {
- /**
- * The organization name you'll see in the browser url
- */
- orgSlug: string;
- /**
- * The project slug of the replayRecord
- */
- projectSlug: string | null;
- /**
- * The replayId
- */
- replayId: string;
- /**
- * The start timestamp of the replay.
- * Used to calculate the offset into the replay from an event timestamp
- */
- replayStartTimestampMs?: number;
- };
- type Result =
- | undefined
- | {
- offsetMs: number;
- highlight?: {
- nodeId: number;
- annotation?: string;
- spotlight?: boolean;
- };
- };
- const ZERO_OFFSET = {offsetMs: 0};
- function fromOffset({offsetSec}): Result {
- if (offsetSec === undefined) {
- // Not using this strategy
- return undefined;
- }
- return {offsetMs: Number(offsetSec) * 1000};
- }
- function fromEventTimestamp({eventTimestamp, replayStartTimestampMs}): Result {
- if (eventTimestamp === undefined) {
- // Not using this strategy
- return undefined;
- }
- if (replayStartTimestampMs !== undefined) {
- const eventTimestampMs = new Date(eventTimestamp).getTime();
- if (eventTimestampMs >= replayStartTimestampMs) {
- return {offsetMs: eventTimestampMs - replayStartTimestampMs};
- }
- }
- // The strategy failed, default to something safe
- return ZERO_OFFSET;
- }
- async function fromListPageQuery({
- api,
- listPageQuery,
- orgSlug,
- replayId,
- projectSlug,
- replayStartTimestampMs,
- }): Promise<Result> {
- if (listPageQuery === undefined) {
- // Not using this strategy
- return undefined;
- }
- // Check if there is even any `click.*` fields in the query string
- const search = new MutableSearch(listPageQuery);
- const isClickSearch = search.getFilterKeys().some(key => key.startsWith('click.'));
- if (!isClickSearch) {
- // There was a search, but not for clicks, so lets skip this strategy.
- return undefined;
- }
- if (replayStartTimestampMs === undefined) {
- // Using the strategy, but we must wait for replayStartTimestampMs to appear
- return ZERO_OFFSET;
- }
- if (!projectSlug) {
- return undefined;
- }
- const results = await fetchReplayClicks({
- api,
- orgSlug,
- projectSlug,
- replayId,
- query: listPageQuery,
- });
- if (!results.clicks.length) {
- return ZERO_OFFSET;
- }
- try {
- const firstResult = first(results.clicks)!;
- const firstTimestamp = firstResult!.timestamp;
- const nodeId = firstResult!.node_id;
- const firstTimestmpMs = new Date(firstTimestamp).getTime();
- return {
- highlight: {
- annotation: listPageQuery,
- nodeId,
- spotlight: true,
- },
- offsetMs: firstTimestmpMs - replayStartTimestampMs,
- };
- } catch {
- return ZERO_OFFSET;
- }
- }
- function useInitialTimeOffsetMs({
- orgSlug,
- replayId,
- projectSlug,
- replayStartTimestampMs,
- }: Opts): Result {
- const api = useApi();
- const {
- query: {event_t: eventTimestamp, query: listPageQuery, t: offsetSec},
- } = useLocation<TimeOffsetLocationQueryParams>();
- const [timestamp, setTimestamp] = useState<Result>(undefined);
- // The different strategies for getting a time offset into the replay (what
- // time to start the replay at)
- // Each strategy should return time in milliseconds
- const offsetTimeMs = useMemo(() => fromOffset({offsetSec}), [offsetSec]);
- const eventTimeMs = useMemo(
- () => fromEventTimestamp({eventTimestamp, replayStartTimestampMs}),
- [eventTimestamp, replayStartTimestampMs]
- );
- const queryTimeMs = useMemo(
- () =>
- eventTimestamp === undefined
- ? fromListPageQuery({
- api,
- listPageQuery,
- orgSlug,
- replayId,
- projectSlug,
- replayStartTimestampMs,
- })
- : undefined,
- [
- api,
- eventTimestamp,
- listPageQuery,
- orgSlug,
- replayId,
- projectSlug,
- replayStartTimestampMs,
- ]
- );
- useEffect(() => {
- Promise.resolve(undefined)
- .then(definedOrDefault(offsetTimeMs))
- .then(definedOrDefault(eventTimeMs))
- .then(definedOrDefault(queryTimeMs))
- .then(definedOrDefault(ZERO_OFFSET))
- .then(setTimestamp);
- }, [offsetTimeMs, eventTimeMs, queryTimeMs, projectSlug]);
- return timestamp;
- }
- function definedOrDefault<T>(dflt: T | undefined | Promise<T | undefined>) {
- return (val: T | undefined) => {
- return val ?? dflt;
- };
- }
- export default useInitialTimeOffsetMs;
|