useReplayRecorder.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import {useCallback, useEffect, useState} from 'react';
  2. import type {ReplayRecordingMode} from '@sentry/core';
  3. import type {replayIntegration} from '@sentry/react';
  4. import useConfiguration from 'sentry/components/devtoolbar/hooks/useConfiguration';
  5. import {useSessionStorage} from 'sentry/utils/useSessionStorage';
  6. type ReplayRecorderState = {
  7. disabledReason: string | undefined;
  8. isDisabled: boolean;
  9. isRecording: boolean;
  10. lastReplayId: string | undefined;
  11. recordingMode: ReplayRecordingMode | undefined;
  12. startRecordingSession(): Promise<boolean>; // returns false if called in the wrong state
  13. stopRecording(): Promise<boolean>; // returns false if called in the wrong state
  14. };
  15. interface ReplayInternalAPI {
  16. [other: string]: any;
  17. getSessionId(): string | undefined;
  18. isEnabled(): boolean;
  19. recordingMode: ReplayRecordingMode;
  20. }
  21. function getReplayInternal(
  22. replay: ReturnType<typeof replayIntegration>
  23. ): ReplayInternalAPI {
  24. // While the toolbar is internal, we can use the private API for added functionality and reduced dependence on SDK release versions
  25. // @ts-ignore:next-line
  26. return replay._replay;
  27. }
  28. const LAST_REPLAY_STORAGE_KEY = 'devtoolbar.last_replay_id';
  29. export default function useReplayRecorder(): ReplayRecorderState {
  30. const {SentrySDK} = useConfiguration();
  31. const replay =
  32. SentrySDK && 'getReplay' in SentrySDK ? SentrySDK.getReplay() : undefined;
  33. const replayInternal = replay ? getReplayInternal(replay) : undefined;
  34. // sessionId is defined if we are recording in session OR buffer mode.
  35. const [sessionId, setSessionId] = useState<string | undefined>(() =>
  36. replayInternal?.getSessionId()
  37. );
  38. const [recordingMode, setRecordingMode] = useState<ReplayRecordingMode | undefined>(
  39. () => replayInternal?.recordingMode
  40. );
  41. const isDisabled = replay === undefined;
  42. const disabledReason = !SentrySDK
  43. ? 'Failed to load the Sentry SDK.'
  44. : !('getReplay' in SentrySDK)
  45. ? 'Your SDK version is too old to support Replays.'
  46. : !replay
  47. ? 'You need to install the SDK Replay integration.'
  48. : undefined;
  49. const [isRecording, setIsRecording] = useState<boolean>(
  50. () => replayInternal?.isEnabled() ?? false
  51. );
  52. const [lastReplayId, setLastReplayId] = useSessionStorage<string | undefined>(
  53. LAST_REPLAY_STORAGE_KEY,
  54. undefined
  55. );
  56. useEffect(() => {
  57. if (isRecording && recordingMode === 'session' && sessionId) {
  58. setLastReplayId(sessionId);
  59. }
  60. }, [isRecording, recordingMode, sessionId, setLastReplayId]);
  61. const refreshState = useCallback(() => {
  62. setIsRecording(replayInternal?.isEnabled() ?? false);
  63. setSessionId(replayInternal?.getSessionId());
  64. setRecordingMode(replayInternal?.recordingMode);
  65. }, [replayInternal]);
  66. const startRecordingSession = useCallback(async () => {
  67. let success = false;
  68. if (replay) {
  69. // Note SDK v8.19 and older will throw if a replay is already started.
  70. // Details at https://github.com/getsentry/sentry-javascript/pull/13000
  71. if (!isRecording) {
  72. replay.start();
  73. success = true;
  74. } else if (recordingMode === 'buffer') {
  75. // For SDK v8.20+, flush() would work for both cases, but we're staying version-agnostic.
  76. await replay.flush();
  77. success = true;
  78. }
  79. refreshState();
  80. }
  81. return success;
  82. }, [replay, isRecording, recordingMode, refreshState]);
  83. const stopRecording = useCallback(async () => {
  84. let success = false;
  85. if (replay && isRecording) {
  86. await replay.stop();
  87. success = true;
  88. refreshState();
  89. }
  90. return success;
  91. }, [isRecording, replay, refreshState]);
  92. return {
  93. disabledReason,
  94. isDisabled,
  95. isRecording,
  96. lastReplayId,
  97. recordingMode,
  98. startRecordingSession,
  99. stopRecording,
  100. };
  101. }