{"version":3,"file":"offline.js","sources":["../../../src/transports/offline.ts"],"sourcesContent":["import { DEBUG_BUILD } from '../debug-build';\nimport type { Envelope } from '../types-hoist/envelope';\nimport type { InternalBaseTransportOptions, Transport, TransportMakeRequestResponse } from '../types-hoist/transport';\nimport { debug } from '../utils/debug-logger';\nimport { envelopeContainsItemType } from '../utils/envelope';\nimport { parseRetryAfterHeader } from '../utils/ratelimit';\nimport { safeUnref } from '../utils/timer';\n\nexport const MIN_DELAY = 100; // 100 ms\nexport const START_DELAY = 5_000; // 5 seconds\nconst MAX_DELAY = 3.6e6; // 1 hour\n\nexport interface OfflineStore {\n  push(env: Envelope): Promise<void>;\n  unshift(env: Envelope): Promise<void>;\n  shift(): Promise<Envelope | undefined>;\n}\n\nexport type CreateOfflineStore = (options: OfflineTransportOptions) => OfflineStore;\n\nexport interface OfflineTransportOptions extends InternalBaseTransportOptions {\n  /**\n   * A function that creates the offline store instance.\n   */\n  createStore?: CreateOfflineStore;\n\n  /**\n   * Flush the offline store shortly after startup.\n   *\n   * Defaults: false\n   */\n  flushAtStartup?: boolean;\n\n  /**\n   * Called before an event is stored.\n   *\n   * Return false to drop the envelope rather than store it.\n   *\n   * @param envelope The envelope that failed to send.\n   * @param error The error that occurred.\n   * @param retryDelay The current retry delay in milliseconds.\n   * @returns Whether the envelope should be stored.\n   */\n  shouldStore?: (envelope: Envelope, error: Error, retryDelay: number) => boolean | Promise<boolean>;\n\n  /**\n   * Should an attempt be made to send the envelope to Sentry.\n   *\n   * If this function is supplied and returns false, `shouldStore` will be called to determine if the envelope should be stored.\n   *\n   * @param envelope The envelope that will be sent.\n   * @returns Whether we should attempt to send the envelope\n   */\n  shouldSend?: (envelope: Envelope) => boolean | Promise<boolean>;\n}\n\ntype Timer = number | { unref?: () => void };\n\n/**\n * Wraps a transport and stores and retries events when they fail to send.\n *\n * @param createTransport The transport to wrap.\n */\nexport function makeOfflineTransport<TO>(\n  createTransport: (options: TO) => Transport,\n): (options: TO & OfflineTransportOptions) => Transport {\n  function log(...args: unknown[]): void {\n    DEBUG_BUILD && debug.log('[Offline]:', ...args);\n  }\n\n  return options => {\n    const transport = createTransport(options);\n\n    if (!options.createStore) {\n      throw new Error('No `createStore` function was provided');\n    }\n\n    const store = options.createStore(options);\n\n    let retryDelay = START_DELAY;\n    let flushTimer: Timer | undefined;\n\n    function shouldQueue(env: Envelope, error: Error, retryDelay: number): boolean | Promise<boolean> {\n      // We want to drop client reports because they can be generated when we retry sending events while offline.\n      if (envelopeContainsItemType(env, ['client_report'])) {\n        return false;\n      }\n\n      if (options.shouldStore) {\n        return options.shouldStore(env, error, retryDelay);\n      }\n\n      return true;\n    }\n\n    function flushIn(delay: number): void {\n      if (flushTimer) {\n        clearTimeout(flushTimer as ReturnType<typeof setTimeout>);\n      }\n\n      // We need to unref the timer in node.js, otherwise the node process never exit.\n      flushTimer = safeUnref(\n        setTimeout(async () => {\n          flushTimer = undefined;\n\n          const found = await store.shift();\n          if (found) {\n            log('Attempting to send previously queued event');\n\n            // We should to update the sent_at timestamp to the current time.\n            found[0].sent_at = new Date().toISOString();\n\n            void send(found, true).catch(e => {\n              log('Failed to retry sending', e);\n            });\n          }\n        }, delay),\n      ) as Timer;\n    }\n\n    function flushWithBackOff(): void {\n      if (flushTimer) {\n        return;\n      }\n\n      flushIn(retryDelay);\n\n      retryDelay = Math.min(retryDelay * 2, MAX_DELAY);\n    }\n\n    async function send(envelope: Envelope, isRetry: boolean = false): Promise<TransportMakeRequestResponse> {\n      // We queue all replay envelopes to avoid multiple replay envelopes being sent at the same time. If one fails, we\n      // need to retry them in order.\n      if (!isRetry && envelopeContainsItemType(envelope, ['replay_event', 'replay_recording'])) {\n        await store.push(envelope);\n        flushIn(MIN_DELAY);\n        return {};\n      }\n\n      try {\n        if (options.shouldSend && (await options.shouldSend(envelope)) === false) {\n          throw new Error('Envelope not sent because `shouldSend` callback returned false');\n        }\n\n        const result = await transport.send(envelope);\n\n        let delay = MIN_DELAY;\n\n        if (result) {\n          // If there's a retry-after header, use that as the next delay.\n          if (result.headers?.['retry-after']) {\n            delay = parseRetryAfterHeader(result.headers['retry-after']);\n          } else if (result.headers?.['x-sentry-rate-limits']) {\n            delay = 60_000; // 60 seconds\n          } // If we have a server error, return now so we don't flush the queue.\n          else if ((result.statusCode || 0) >= 400) {\n            return result;\n          }\n        }\n\n        flushIn(delay);\n        retryDelay = START_DELAY;\n        return result;\n      } catch (e) {\n        if (await shouldQueue(envelope, e as Error, retryDelay)) {\n          // If this envelope was a retry, we want to add it to the front of the queue so it's retried again first.\n          if (isRetry) {\n            await store.unshift(envelope);\n          } else {\n            await store.push(envelope);\n          }\n          flushWithBackOff();\n          log('Error sending. Event queued.', e as Error);\n          return {};\n        } else {\n          throw e;\n        }\n      }\n    }\n\n    if (options.flushAtStartup) {\n      flushWithBackOff();\n    }\n\n    return {\n      send,\n      flush: timeout => {\n        // If there's no timeout, we should attempt to flush the offline queue.\n        if (timeout === undefined) {\n          retryDelay = START_DELAY;\n          flushIn(MIN_DELAY);\n        }\n\n        return transport.flush(timeout);\n      },\n    };\n  };\n}\n"],"names":["DEBUG_BUILD","debug","envelopeContainsItemType","safeUnref","envelope","parseRetryAfterHeader"],"mappings":"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"}