Ctrl K
ComunidadGuías…

src/services/tracking.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { defaultApiHost, useConsole } from '~/services/console'

const KEEPALIVE_BODY_LIMIT = 64 * 1024
let reservedKeepaliveBytes = 0

function reserveKeepalive(bodySize: number): boolean {
  if (reservedKeepaliveBytes + bodySize > KEEPALIVE_BODY_LIMIT)
    return false

  reservedKeepaliveBytes += bodySize
  return true
}

function releaseKeepalive(bodySize: number): void {
  reservedKeepaliveBytes -= bodySize
}

type TagKey = Lowercase<string>
/** Tag Type */
type Tags = Record<TagKey, string | number | boolean>
/**
 * Options for publishing analytics events
 */
interface TrackOptions {
  /** Stable identity for this event across delivery attempts. */
  client_event_id?: string
  /**
   * Channel name
   * example: "waitlist"
   */
  channel: string
  /**
   * Event name
   * example: "User Joined"
   */
  event: string
  /**
   * Event description
   * example: "joe@example.com joined waitlist"
   */
  description?: string
  /**
   * User ID
   * example: "user-123"
   */
  user_id?: string
  /**
   * Organization ID for actor-scoped tracking.
   */
  org_id?: string
  /**
   * Tracking payload contract version.
   */
  tracking_version?: number
  /**
   * Event tags
   * example: { username: "mattie" }
   */
  tags?: Tags
  /**
   * Per-event metadata that must not become PostHog person properties.
   */
  nonPersonTags?: Tags
  /**
   * Event timestamp
   */
  timestamp?: number | Date
}

export async function sendEvent(payload: TrackOptions): Promise<null> {
  try {
    const { data: currentSession } = await useConsole().auth.getSession()
    if (!currentSession.session)
      return null

    const currentJwt = currentSession.session.access_token
    const clientEventId = payload.client_event_id ?? crypto.randomUUID()
    const timestamp = payload.timestamp instanceof Date ? payload.timestamp.getTime() : payload.timestamp ?? Date.now()
    const body = JSON.stringify({ ...payload, client_event_id: clientEventId, timestamp })
    const bodySize = new TextEncoder().encode(body).byteLength

    // Implement retry logic (3 attempts)
    for (let attempt = 0; attempt < 3; attempt++) {
      // Browsers reject keepalive requests once their in-flight bodies exceed 64 KiB.
      const keepalive = reserveKeepalive(bodySize)
      const controller = new AbortController()
      const timeoutId = setTimeout(() => controller.abort(), 10000)

      try {
        // 10 second timeout using AbortSignal
        const response = await fetch(`${defaultApiHost}/private/events`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${currentJwt}`,
            'Content-Type': 'application/json',
          },
          body,
          keepalive,
          signal: controller.signal,
        })

        // Consume response to avoid memory leaks, but don't throw on errors
        if (!response.ok) {
          await response.text().catch(() => {})
          // Retry on server errors (5xx)
          if (response.status >= 500 && attempt < 2) {
            continue
          }
        }

        return null
      }
      catch (error) {
        // If it's a timeout or network error and we have retries left, continue
        if (attempt < 2 && (error instanceof Error && (error.name === 'AbortError' || error.message.includes('fetch')))) {
          continue
        }
        // Last attempt failed, return null
        return null
      }
      finally {
        clearTimeout(timeoutId)
        if (keepalive)
          releaseKeepalive(bodySize)
      }
    }

    return null
  }
  catch {
    return null
  }
}