{"version":3,"file":"EmbraceSpanStorage.cjs","names":["diag","BasicTracerProvider","KEY_EMB_MAX_PENDING_SPANS_REACHED"],"sources":["../../../src/utils/EmbraceSpanStorage/EmbraceSpanStorage.ts"],"sourcesContent":["import type { DiagLogger, Tracer } from '@opentelemetry/api';\nimport { diag } from '@opentelemetry/api';\nimport type { Resource } from '@opentelemetry/resources';\nimport { emptyResource } from '@opentelemetry/resources';\nimport type { ReadableSpan } from '@opentelemetry/sdk-trace-web';\nimport { BasicTracerProvider } from '@opentelemetry/sdk-trace-web';\nimport { KEY_EMB_MAX_PENDING_SPANS_REACHED } from '../../constants/index.ts';\nimport type { SpanSessionManagerInternal } from '../../managers/index.ts';\n\nconst PENDING_SPANS_STORAGE_KEY_PREFIX = 'embrace_pending_';\nconst MAX_PENDING_SPANS_ITEMS = 10;\n\nexport interface SpanStorageOptions {\n  spanSessionManager: SpanSessionManagerInternal;\n  resource?: Resource;\n  storage?: Storage;\n  diag?: DiagLogger;\n  storedSpansExpireTimeoutMS?: number;\n  onExpiredSpansExport?: (spans: ReadableSpan[]) => void;\n}\n\n// EmbraceSpanStorage is used to keep pending spans that are not meant to be exported (yet) in localStorage,\n// and periodically checks whenever those stored spans are ready to be exported.\n// This functionality has some custom logic to be able to store these spans as JSONs and then recreate them back\n// that could eventually be improved. In particular the things that would need to be improved are:\n// - Avoid serializing as JSON and instead try to use a customExporter that produces the body as it was going to be\n//   sent over the wire (serialized, compressed, etc), and store that, so that later when it decides to export those,\n//   it's just using that body as a regular fetch call.\n// - Create a service worker that is responsible for tracking the pending spans and deciding when to actually export them.\n//   That way we would avoid a potential race condition where two different tabs would try to export the same data.\n// - Make this a more comprehensive component so that the EmbraceSessionBatchedSpanProcessor doesn't need to be in the\n//   middle of SpanSessionVisibilityInstrumentation and this component. Ideally this component would know when and how\n//   to take snapshots of the current pending spans and the current session span at any moment, so that the visibility\n//   instrumentation doesn't need to be telling that to the batch span processor.\nexport class EmbraceSpanStorage {\n  private readonly _noExportTracer: Tracer;\n  private readonly _storage: Storage;\n  private readonly _diag: DiagLogger;\n  private readonly _onExpiredSpansExport?: (spans: ReadableSpan[]) => void;\n  private readonly _storedSpansExpireTimeoutMS: number;\n  private readonly _spanSessionManager: SpanSessionManagerInternal;\n  private _checkExpiredSpansInterval?: ReturnType<typeof setInterval>;\n\n  public constructor({\n    resource = emptyResource(),\n    storage = window.localStorage,\n    diag: diagParam = diag.createComponentLogger({\n      namespace: 'EmbraceSpanStorage',\n    }),\n    storedSpansExpireTimeoutMS = 60 * 60 * 1000, // 1 hour\n    onExpiredSpansExport,\n    spanSessionManager,\n  }: SpanStorageOptions) {\n    this._noExportTracer = new BasicTracerProvider({ resource }).getTracer(\n      'embrace-web-sdk-sessions',\n    );\n    this._storage = storage;\n    this._diag = diagParam;\n    this._storedSpansExpireTimeoutMS = storedSpansExpireTimeoutMS;\n    this._onExpiredSpansExport = onExpiredSpansExport;\n    this._spanSessionManager = spanSessionManager;\n\n    this.startExpiredSpansCheck();\n  }\n\n  public storePendingSpans(\n    sessionId: string,\n    sessionSpan: ReadableSpan,\n    pendingSpans: ReadableSpan[],\n  ): void {\n    try {\n      // If this session was already stored, clear it first:\n      this.clearStoredSpans(sessionId);\n\n      if (this._getPendingSpansKeys().length >= MAX_PENDING_SPANS_ITEMS) {\n        this._spanSessionManager.incrSessionCountForKey(\n          KEY_EMB_MAX_PENDING_SPANS_REACHED,\n        );\n        this._diag.warn(\n          'Not storing pending spans as the max number of items was reached',\n        );\n        return;\n      }\n\n      const key = `${PENDING_SPANS_STORAGE_KEY_PREFIX}${sessionId}_${Date.now()}`;\n      this._storage.setItem(\n        key,\n        JSON.stringify([sessionSpan, ...pendingSpans], (key, value) =>\n          key.startsWith('_') ? undefined : value,\n        ),\n      );\n\n      this._diag.debug(`Stored pending span: ${key}`);\n    } catch (error) {\n      this._diag.error('Failed to store spans to storage:', error);\n    }\n  }\n\n  public clearStoredSpans(sessionId: string): void {\n    try {\n      const prefix = `${PENDING_SPANS_STORAGE_KEY_PREFIX}${sessionId}_`;\n      this._getPendingSpansKeys().forEach((key) => {\n        if (key.startsWith(prefix)) {\n          this._storage.removeItem(key);\n        }\n      });\n    } catch (error) {\n      this._diag.error('Failed to clear stored spans from storage:', error);\n    }\n  }\n\n  public startExpiredSpansCheck(): void {\n    this._checkExpiredSpansInterval = setInterval(\n      () => {\n        this.checkAndExportExpiredSpans();\n      },\n      5 * 60 * 1000,\n    ); // Check every 5 minutes\n  }\n\n  public stopExpiredSpansCheck(): void {\n    if (this._checkExpiredSpansInterval) {\n      clearInterval(this._checkExpiredSpansInterval);\n      this._checkExpiredSpansInterval = undefined;\n    }\n  }\n\n  public checkAndExportExpiredSpans(): void {\n    try {\n      const keys = this._getPendingSpansKeys();\n      if (keys.length === 0) {\n        return;\n      }\n\n      const currentTime = Date.now();\n      keys.forEach((key) => {\n        const parts = key.split('_');\n        const storedTime = parseInt(parts[parts.length - 1], 10);\n\n        if (Number.isNaN(storedTime)) {\n          this._diag.error(\n            'Found invalid timestamp in stored span:',\n            storedTime,\n          );\n          this._storage.removeItem(key);\n        }\n\n        if (currentTime - storedTime <= this._storedSpansExpireTimeoutMS) {\n          return;\n        }\n\n        const storedData = this._storage.getItem(key);\n        if (!storedData) return;\n\n        try {\n          const spans: ReadableSpan[] = [];\n          for (const storedSpan of JSON.parse(storedData) as ReadableSpan[]) {\n            const span = this._noExportTracer.startSpan(storedSpan.name, {\n              kind: storedSpan.kind,\n              attributes: storedSpan.attributes,\n              links: storedSpan.links,\n              startTime: storedSpan.startTime,\n            });\n            span.setStatus(storedSpan.status);\n            span.end(storedSpan.endTime);\n            spans.push(span as unknown as ReadableSpan);\n          }\n\n          if (this._onExpiredSpansExport && spans.length > 0) {\n            this._onExpiredSpansExport(spans);\n          }\n        } catch (e) {\n          this._diag.error('Failed to process expired spans:', e);\n        }\n\n        // Always remove the storage item even if processing failed\n        this._storage.removeItem(key);\n      });\n    } catch (e) {\n      this._diag.error('Failed to check and export expired spans:', e);\n    }\n  }\n\n  public _getPendingSpansKeys(): string[] {\n    const keys: string[] = [];\n    for (let i = 0; i < this._storage.length; i++) {\n      const key = this._storage.key(i);\n      if (key?.startsWith(PENDING_SPANS_STORAGE_KEY_PREFIX)) {\n        keys.push(key);\n      }\n    }\n    return keys;\n  }\n\n  public destroy(): void {\n    this.stopExpiredSpansCheck();\n  }\n}\n"],"mappings":"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"}