useInitialTimeOffsetMs.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import {useEffect, useMemo, useState} from 'react';
  2. import first from 'lodash/first';
  3. import fetchReplayClicks from 'sentry/utils/replays/fetchReplayClicks';
  4. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  5. import useApi from 'sentry/utils/useApi';
  6. import {useLocation} from 'sentry/utils/useLocation';
  7. export type TimeOffsetLocationQueryParams = {
  8. /**
  9. * The time when the event happened.
  10. * Anything that can be parsed by `new Date()`; for example a timestamp in ms
  11. * or an ISO 8601 formatted string.
  12. */
  13. event_t?: string;
  14. /**
  15. * The query that was used on the index page. If it includes `click.*` fields
  16. * then we will use those to lookup a list of `offsetMs` values
  17. */
  18. query?: string;
  19. /**
  20. * A specific offset into the replay. Number of seconds.
  21. * Should be less than the duration of the replay
  22. */
  23. t?: string;
  24. };
  25. type Opts = {
  26. /**
  27. * The organization name you'll see in the browser url
  28. */
  29. orgSlug: string;
  30. /**
  31. * The project slug of the replayRecord
  32. */
  33. projectSlug: string | null;
  34. /**
  35. * The replayId
  36. */
  37. replayId: string;
  38. /**
  39. * The start timestamp of the replay.
  40. * Used to calculate the offset into the replay from an event timestamp
  41. */
  42. replayStartTimestampMs?: number;
  43. };
  44. type Result =
  45. | undefined
  46. | {
  47. offsetMs: number;
  48. highlight?: {
  49. nodeId: number;
  50. annotation?: string;
  51. spotlight?: boolean;
  52. };
  53. };
  54. const ZERO_OFFSET = {offsetMs: 0};
  55. function fromOffset({offsetSec}): Result {
  56. if (offsetSec === undefined) {
  57. // Not using this strategy
  58. return undefined;
  59. }
  60. return {offsetMs: Number(offsetSec) * 1000};
  61. }
  62. function fromEventTimestamp({eventTimestamp, replayStartTimestampMs}): Result {
  63. if (eventTimestamp === undefined) {
  64. // Not using this strategy
  65. return undefined;
  66. }
  67. if (replayStartTimestampMs !== undefined) {
  68. const eventTimestampMs = new Date(eventTimestamp).getTime();
  69. if (eventTimestampMs >= replayStartTimestampMs) {
  70. return {offsetMs: eventTimestampMs - replayStartTimestampMs};
  71. }
  72. }
  73. // The strategy failed, default to something safe
  74. return ZERO_OFFSET;
  75. }
  76. async function fromListPageQuery({
  77. api,
  78. listPageQuery,
  79. orgSlug,
  80. replayId,
  81. projectSlug,
  82. replayStartTimestampMs,
  83. }): Promise<Result> {
  84. if (listPageQuery === undefined) {
  85. // Not using this strategy
  86. return undefined;
  87. }
  88. // Check if there is even any `click.*` fields in the query string
  89. const search = new MutableSearch(listPageQuery);
  90. const isClickSearch = search.getFilterKeys().some(key => key.startsWith('click.'));
  91. if (!isClickSearch) {
  92. // There was a search, but not for clicks, so lets skip this strategy.
  93. return undefined;
  94. }
  95. if (replayStartTimestampMs === undefined) {
  96. // Using the strategy, but we must wait for replayStartTimestampMs to appear
  97. return ZERO_OFFSET;
  98. }
  99. if (!projectSlug) {
  100. return undefined;
  101. }
  102. const results = await fetchReplayClicks({
  103. api,
  104. orgSlug,
  105. projectSlug,
  106. replayId,
  107. query: listPageQuery,
  108. });
  109. if (!results.clicks.length) {
  110. return ZERO_OFFSET;
  111. }
  112. try {
  113. const firstResult = first(results.clicks)!;
  114. const firstTimestamp = firstResult!.timestamp;
  115. const nodeId = firstResult!.node_id;
  116. const firstTimestmpMs = new Date(firstTimestamp).getTime();
  117. return {
  118. highlight: {
  119. annotation: listPageQuery,
  120. nodeId,
  121. spotlight: true,
  122. },
  123. offsetMs: firstTimestmpMs - replayStartTimestampMs,
  124. };
  125. } catch {
  126. return ZERO_OFFSET;
  127. }
  128. }
  129. function useInitialTimeOffsetMs({
  130. orgSlug,
  131. replayId,
  132. projectSlug,
  133. replayStartTimestampMs,
  134. }: Opts): Result {
  135. const api = useApi();
  136. const {
  137. query: {event_t: eventTimestamp, query: listPageQuery, t: offsetSec},
  138. } = useLocation<TimeOffsetLocationQueryParams>();
  139. const [timestamp, setTimestamp] = useState<Result>(undefined);
  140. // The different strategies for getting a time offset into the replay (what
  141. // time to start the replay at)
  142. // Each strategy should return time in milliseconds
  143. const offsetTimeMs = useMemo(() => fromOffset({offsetSec}), [offsetSec]);
  144. const eventTimeMs = useMemo(
  145. () => fromEventTimestamp({eventTimestamp, replayStartTimestampMs}),
  146. [eventTimestamp, replayStartTimestampMs]
  147. );
  148. const queryTimeMs = useMemo(
  149. () =>
  150. eventTimestamp === undefined
  151. ? fromListPageQuery({
  152. api,
  153. listPageQuery,
  154. orgSlug,
  155. replayId,
  156. projectSlug,
  157. replayStartTimestampMs,
  158. })
  159. : undefined,
  160. [
  161. api,
  162. eventTimestamp,
  163. listPageQuery,
  164. orgSlug,
  165. replayId,
  166. projectSlug,
  167. replayStartTimestampMs,
  168. ]
  169. );
  170. useEffect(() => {
  171. Promise.resolve(undefined)
  172. .then(definedOrDefault(offsetTimeMs))
  173. .then(definedOrDefault(eventTimeMs))
  174. .then(definedOrDefault(queryTimeMs))
  175. .then(definedOrDefault(ZERO_OFFSET))
  176. .then(setTimestamp);
  177. }, [offsetTimeMs, eventTimeMs, queryTimeMs, projectSlug]);
  178. return timestamp;
  179. }
  180. function definedOrDefault<T>(dflt: T | undefined | Promise<T | undefined>) {
  181. return (val: T | undefined) => {
  182. return val ?? dflt;
  183. };
  184. }
  185. export default useInitialTimeOffsetMs;