{"version":3,"file":"slack.cjs","names":["EventSubscriber"],"sources":["../src/slack.ts"],"sourcesContent":["/**\n * Slack Subscriber for autotel\n *\n * Send events events as notifications to Slack channels via webhooks.\n *\n * Perfect for:\n * - Critical business events (orders, payments, signups)\n * - Real-time alerts for failures\n * - Team notifications for important milestones\n * - Monitoring funnel completions\n *\n * @example Basic usage\n * ```typescript\n * import { Events } from 'autotel/events';\n * import { SlackSubscriber } from 'autotel-subscribers/slack';\n *\n * const events = new Events('app', {\n *   subscribers: [\n *     new SlackSubscriber({\n *       webhookUrl: process.env.SLACK_WEBHOOK_URL!,\n *       channel: '#order-events'\n *     })\n *   ]\n * });\n *\n * // Sends to Slack\n * events.trackEvent('order.completed', {\n *   orderId: 'ord_123',\n *   userId: 'user_456',\n *   amount: 99.99\n * });\n * ```\n *\n * @example Filter critical events only\n * ```typescript\n * const events = new Events('app', {\n *   subscribers: [\n *     new SlackSubscriber({\n *       webhookUrl: process.env.SLACK_WEBHOOK_URL!,\n *       channel: '#alerts',\n *       filter: (payload) => {\n *         // Only send failures and high-value orders\n *         if (payload.type === 'outcome' && payload.outcome === 'failure') {\n *           return true;\n *         }\n *         if (payload.name === 'order.completed' && payload.attributes?.amount > 1000) {\n *           return true;\n *         }\n *         return false;\n *       }\n *     })\n *   ]\n * });\n * ```\n *\n * Setup:\n * 1. Create Slack App: https://api.slack.com/apps\n * 2. Enable Incoming Webhooks\n * 3. Add webhook to workspace\n * 4. Copy webhook URL (https://hooks.slack.com/services/...)\n */\n\nimport {\n  EventSubscriber,\n  type EventPayload,\n} from './event-subscriber-base';\n\nexport interface SlackSubscriberConfig {\n  /** Slack webhook URL (https://hooks.slack.com/services/...) */\n  webhookUrl: string;\n\n  /** Default channel to post to (optional, overrides webhook default) */\n  channel?: string;\n\n  /** Custom username for bot (default: 'Events Bot') */\n  username?: string;\n\n  /** Custom emoji icon (default: ':chart_with_upwards_trend:') */\n  iconEmoji?: string;\n\n  /** Include timestamp in messages (default: true) */\n  includeTimestamp?: boolean;\n\n  /** Include event attributes as fields (default: true) */\n  includeAttributes?: boolean;\n\n  /** Maximum attributes to show (default: 10) */\n  maxAttributeFields?: number;\n\n  /** Filter function - return true to send, false to skip */\n  filter?: (payload: EventPayload) => boolean;\n\n  /** Enable/disable subscriber */\n  enabled?: boolean;\n}\n\ninterface SlackMessage {\n  channel?: string;\n  username?: string;\n  icon_emoji?: string;\n  text?: string;\n  attachments: SlackAttachment[];\n}\n\ninterface SlackAttachment {\n  color?: string;\n  title?: string;\n  text?: string;\n  fields?: SlackField[];\n  footer?: string;\n  footer_icon?: string;\n  ts?: number;\n}\n\ninterface SlackField {\n  title: string;\n  value: string;\n  short: boolean;\n}\n\nexport class SlackSubscriber extends EventSubscriber {\n  readonly name = 'SlackSubscriber';\n  readonly version = '1.0.0';\n\n  private config: Required<Omit<SlackSubscriberConfig, 'channel' | 'filter'>> & {\n    channel?: string;\n    filter?: (payload: EventPayload) => boolean;\n  };\n\n  constructor(config: SlackSubscriberConfig) {\n    super();\n\n    this.config = {\n      webhookUrl: config.webhookUrl,\n      channel: config.channel,\n      username: config.username ?? 'Events Bot',\n      iconEmoji: config.iconEmoji ?? ':chart_with_upwards_trend:',\n      includeTimestamp: config.includeTimestamp ?? true,\n      includeAttributes: config.includeAttributes ?? true,\n      maxAttributeFields: config.maxAttributeFields ?? 10,\n      filter: config.filter,\n      enabled: config.enabled ?? true,\n    };\n\n    this.enabled = this.config.enabled;\n\n    if (!this.config.webhookUrl) {\n      console.error(\n        '[SlackSubscriber] No webhook URL provided - subscriber disabled'\n      );\n      this.enabled = false;\n    }\n  }\n\n  protected async sendToDestination(payload: EventPayload): Promise<void> {\n    // Apply filter if provided\n    if (this.config.filter) {\n      const filterFn = this.config.filter;\n      const shouldInclude = filterFn(payload);\n      if (!shouldInclude) {\n        return; // Skip this event\n      }\n    }\n\n    const message = this.formatSlackMessage(payload);\n\n    const response = await fetch(this.config.webhookUrl, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify(message),\n    });\n\n    if (!response.ok) {\n      const errorText = await response.text();\n      throw new Error(\n        `Slack webhook failed (${response.status}): ${errorText}`\n      );\n    }\n  }\n\n  /**\n   * Format events payload as Slack message\n   */\n  private formatSlackMessage(payload: EventPayload): SlackMessage {\n    const emoji = this.getEventEmoji(payload);\n    const color = this.getEventColor(payload);\n    const title = `${emoji} ${payload.name}`;\n\n    // Add event type\n    const fields: SlackField[] = [\n      {\n        title: 'Event Type',\n        value: this.formatEventType(payload),\n        short: true,\n      },\n    ];\n\n    // Add timestamp if enabled\n    if (this.config.includeTimestamp) {\n      fields.push({\n        title: 'Timestamp',\n        value: new Date(payload.timestamp).toLocaleString('en-US', {\n          month: 'short',\n          day: 'numeric',\n          hour: '2-digit',\n          minute: '2-digit',\n          second: '2-digit',\n        }),\n        short: true,\n      });\n    }\n\n    // Add attributes as fields\n    if (this.config.includeAttributes && payload.attributes) {\n      const attributeFields = this.formatAttributes(payload.attributes);\n      fields.push(...attributeFields);\n    }\n\n    const attachment: SlackAttachment = {\n      color,\n      title,\n      fields,\n      footer: 'Events Events',\n      footer_icon: 'https://i.imgur.com/QpCKbNL.png',\n    };\n\n    // Add Unix timestamp for Slack\n    if (this.config.includeTimestamp) {\n      attachment.ts = Math.floor(\n        new Date(payload.timestamp).getTime() / 1000\n      );\n    }\n\n    return {\n      channel: this.config.channel,\n      username: this.config.username,\n      icon_emoji: this.config.iconEmoji,\n      attachments: [attachment],\n    };\n  }\n\n  /**\n   * Get emoji for event type\n   */\n  private getEventEmoji(payload: EventPayload): string {\n    switch (payload.type) {\n      case 'outcome': {\n        return payload.outcome === 'success' ? '✅' : '❌';\n      }\n      case 'funnel': {\n        return '🔄';\n      }\n      case 'value': {\n        return '📊';\n      }\n      default: {\n        // Use custom emoji for common event patterns\n        if (payload.name.includes('order') || payload.name.includes('payment'))\n          return '💰';\n        if (payload.name.includes('signup') || payload.name.includes('user'))\n          return '👤';\n        if (payload.name.includes('error') || payload.name.includes('fail'))\n          return '⚠️';\n        return '📌';\n      }\n    }\n  }\n\n  /**\n   * Get Slack attachment color for event type\n   */\n  private getEventColor(payload: EventPayload): string {\n    switch (payload.type) {\n      case 'outcome': {\n        return payload.outcome === 'success' ? 'good' : 'danger';\n      } // Green or red\n      case 'funnel': {\n        return '#3AA3E3';\n      } // Blue\n      case 'value': {\n        return '#764FA5';\n      } // Purple\n      default: {\n        // Custom colors for patterns\n        if (payload.name.includes('error') || payload.name.includes('fail'))\n          return 'danger';\n        if (payload.name.includes('warning')) return 'warning';\n        return 'good';\n      } // Default green\n    }\n  }\n\n  /**\n   * Format event type for display\n   */\n  private formatEventType(payload: EventPayload): string {\n    switch (payload.type) {\n      case 'event': {\n        return 'Event';\n      }\n      case 'funnel': {\n        return `Funnel: ${payload.step || 'unknown'}`;\n      }\n      case 'outcome': {\n        return `Outcome: ${payload.outcome || 'unknown'}`;\n      }\n      case 'value': {\n        return `Value: ${payload.value ?? 'N/A'}`;\n      }\n      default: {\n        return payload.type;\n      }\n    }\n  }\n\n  /**\n   * Format attributes as Slack fields\n   */\n  private formatAttributes(attributes: Record<string, any>): SlackField[] {\n    const fields: SlackField[] = [];\n    const entries = Object.entries(attributes);\n\n    // Limit number of fields\n    const limit = Math.min(entries.length, this.config.maxAttributeFields);\n\n    for (let i = 0; i < limit; i++) {\n      const [key, value] = entries[i];\n\n      // Skip internal/system fields\n      if (key.startsWith('_') || key === 'timestamp') continue;\n\n      fields.push({\n        title: this.formatFieldName(key),\n        value: this.formatFieldValue(value),\n        short: true,\n      });\n    }\n\n    // Add truncation notice if needed\n    if (entries.length > this.config.maxAttributeFields) {\n      fields.push({\n        title: 'Note',\n        value: `... and ${entries.length - this.config.maxAttributeFields} more fields`,\n        short: false,\n      });\n    }\n\n    return fields;\n  }\n\n  /**\n   * Format field name (convert camelCase to Title Case)\n   */\n  private formatFieldName(name: string): string {\n    return name\n      .replaceAll(/([A-Z])/g, ' $1') // Add space before capitals\n      .replace(/^./, (str) => str.toUpperCase()) // Capitalize first letter\n      .trim();\n  }\n\n  /**\n   * Format field value\n   */\n  private formatFieldValue(value: any): string {\n    if (value === null || value === undefined) return 'N/A';\n    if (typeof value === 'boolean') return value ? 'Yes' : 'No';\n    if (typeof value === 'object') return JSON.stringify(value);\n    if (typeof value === 'number' && !Number.isInteger(value)) {\n      return value.toFixed(2);\n    }\n    return String(value);\n  }\n\n  /**\n   * Handle errors (override from EventSubscriber)\n   */\n  protected handleError(error: Error, payload: EventPayload): void {\n    console.error(\n      `[SlackSubscriber] Failed to send ${payload.type} event \"${payload.name}\":`,\n      error\n    );\n  }\n}\n"],"mappings":"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"}