Ctrl K
ComunidadGuías…

src/services/organizationInvitationTelemetry.ts

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

Archivo original ↗Preguntar a KiBot
import { getLocalConfig } from '~/services/console'
import { pushEventForUser } from '~/services/posthog'

type OrganizationInvitationSuccessEvent
  = 'organization_membership_invitation_accepted'
    | 'organization_membership_invitation_declined'

type OrganizationInvitationEvent
  = OrganizationInvitationSuccessEvent
    | 'organization_membership_invitation_failed'
    | 'organization_membership_invitation_skipped'
    | 'organization_membership_invitation_viewed'

export type OrganizationInvitationFailureReason
  = 'acceptance_failed'
    | 'decline_failed'
    | 'invitation_not_found'
    | 'membership_update_failed'
    | 'unknown'

interface OrganizationInvitationEventInput {
  event: OrganizationInvitationEvent
  failureReason?: OrganizationInvitationFailureReason
  invitationId?: string
  pendingInvitationCount: number
  userId: string
}

interface TrackedOrganizationInvitationMutationInput {
  failureReason: Extract<OrganizationInvitationFailureReason, 'acceptance_failed' | 'decline_failed'>
  pendingInvitationCount: number
  successEvent: OrganizationInvitationSuccessEvent
  userId: string
}

const FAILURE_REASON_BY_CODE: Record<string, OrganizationInvitationFailureReason> = {
  FAILED_TO_ACCEPT_INVITATION: 'membership_update_failed',
  NO_INVITE: 'invitation_not_found',
  ROLE_NOT_FOUND: 'membership_update_failed',
}

function failureCode(error: unknown): string {
  if (typeof error === 'string')
    return error.trim().toUpperCase()

  if (!error || typeof error !== 'object')
    return ''

  const record = error as { code?: unknown, error?: unknown, message?: unknown }
  const candidate = record.error ?? record.code ?? record.message
  return typeof candidate === 'string' ? candidate.trim().toUpperCase() : ''
}

export function sanitizeOrganizationInvitationClientFailureReason(
  error: unknown,
  fallback: OrganizationInvitationFailureReason = 'unknown',
): OrganizationInvitationFailureReason {
  return FAILURE_REASON_BY_CODE[failureCode(error)] ?? fallback
}

function invitationInsertId(event: OrganizationInvitationSuccessEvent, userId: string, invitationId: string): string {
  return `organization-invitation:${event}:authenticated_pending_invite:${userId}:${invitationId}`
}

export function captureOrganizationInvitationEvent(input: OrganizationInvitationEventInput): void {
  try {
    pushEventForUser(
      input.event,
      input.userId,
      getLocalConfig().apiHost,
      {
        ...(input.invitationId && (input.event === 'organization_membership_invitation_accepted' || input.event === 'organization_membership_invitation_declined')
          ? { $insert_id: invitationInsertId(input.event, input.userId, input.invitationId) }
          : {}),
        account_state: 'already_existed',
        entry_path: '/onboarding/invitation',
        ...(input.failureReason ? { failure_reason: input.failureReason } : {}),
        flow: 'authenticated_pending_invite',
        pending_invitation_count: input.pendingInvitationCount,
      },
    )
  }
  catch {
    // Invitation actions must never depend on analytics availability.
  }
}

export async function runTrackedOrganizationInvitationMutation(
  input: TrackedOrganizationInvitationMutationInput,
  mutation: () => Promise<string | void>,
): Promise<void> {
  let invitationId: string | void
  try {
    invitationId = await mutation()
  }
  catch (error) {
    captureOrganizationInvitationEvent({
      event: 'organization_membership_invitation_failed',
      failureReason: sanitizeOrganizationInvitationClientFailureReason(error, input.failureReason),
      pendingInvitationCount: input.pendingInvitationCount,
      userId: input.userId,
    })
    throw error
  }

  captureOrganizationInvitationEvent({
    event: input.successEvent,
    invitationId: invitationId ?? undefined,
    pendingInvitationCount: input.pendingInvitationCount,
    userId: input.userId,
  })
}