posthog-webhooks-events

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PostHog Webhooks & Events

PostHog Webhooks与事件处理

Overview

概述

Securely handle PostHog webhooks with signature validation and replay protection.
通过签名验证和重放攻击防护,安全处理PostHog webhook。

Prerequisites

前置条件

  • PostHog webhook secret configured
  • HTTPS endpoint accessible from internet
  • Understanding of cryptographic signatures
  • Redis or database for idempotency (optional)
  • 已配置PostHog webhook密钥
  • 可从互联网访问的HTTPS端点
  • 了解加密签名相关知识
  • Redis或数据库(用于幂等性处理,可选)

Webhook Endpoint Setup

Webhook端点设置

Express.js

Express.js

typescript
import express from 'express';
import crypto from 'crypto';

const app = express();

// IMPORTANT: Raw body needed for signature verification
app.post('/webhooks/posthog',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.headers['x-posthog-signature'] as string;
    const timestamp = req.headers['x-posthog-timestamp'] as string;

    if (!verifyPostHogSignature(req.body, signature, timestamp)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(req.body.toString());
    await handlePostHogEvent(event);

    res.status(200).json({ received: true });
  }
);
typescript
import express from 'express';
import crypto from 'crypto';

const app = express();

// IMPORTANT: Raw body needed for signature verification
app.post('/webhooks/posthog',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.headers['x-posthog-signature'] as string;
    const timestamp = req.headers['x-posthog-timestamp'] as string;

    if (!verifyPostHogSignature(req.body, signature, timestamp)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(req.body.toString());
    await handlePostHogEvent(event);

    res.status(200).json({ received: true });
  }
);

Signature Verification

签名验证

typescript
function verifyPostHogSignature(
  payload: Buffer,
  signature: string,
  timestamp: string
): boolean {
  const secret = process.env.POSTHOG_WEBHOOK_SECRET!;

  // Reject old timestamps (replay attack protection)
  const timestampAge = Date.now() - parseInt(timestamp) * 1000;
  if (timestampAge > 300000) { // 5 minutes
    console.error('Webhook timestamp too old');
    return false;
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload.toString()}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
typescript
function verifyPostHogSignature(
  payload: Buffer,
  signature: string,
  timestamp: string
): boolean {
  const secret = process.env.POSTHOG_WEBHOOK_SECRET!;

  // Reject old timestamps (replay attack protection)
  const timestampAge = Date.now() - parseInt(timestamp) * 1000;
  if (timestampAge > 300000) { // 5 minutes
    console.error('Webhook timestamp too old');
    return false;
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload.toString()}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

Event Handler Pattern

事件处理模式

typescript
type PostHogEventType = 'resource.created' | 'resource.updated' | 'resource.deleted';

interface PostHogEvent {
  id: string;
  type: PostHogEventType;
  data: Record<string, any>;
  created: string;
}

const eventHandlers: Record<PostHogEventType, (data: any) => Promise<void>> = {
  'resource.created': async (data) => { /* handle */ },
  'resource.updated': async (data) => { /* handle */ },
  'resource.deleted': async (data) => { /* handle */ }
};

async function handlePostHogEvent(event: PostHogEvent): Promise<void> {
  const handler = eventHandlers[event.type];

  if (!handler) {
    console.log(`Unhandled event type: ${event.type}`);
    return;
  }

  try {
    await handler(event.data);
    console.log(`Processed ${event.type}: ${event.id}`);
  } catch (error) {
    console.error(`Failed to process ${event.type}: ${event.id}`, error);
    throw error; // Rethrow to trigger retry
  }
}
typescript
type PostHogEventType = 'resource.created' | 'resource.updated' | 'resource.deleted';

interface PostHogEvent {
  id: string;
  type: PostHogEventType;
  data: Record<string, any>;
  created: string;
}

const eventHandlers: Record<PostHogEventType, (data: any) => Promise<void>> = {
  'resource.created': async (data) => { /* handle */ },
  'resource.updated': async (data) => { /* handle */ },
  'resource.deleted': async (data) => { /* handle */ }
};

async function handlePostHogEvent(event: PostHogEvent): Promise<void> {
  const handler = eventHandlers[event.type];

  if (!handler) {
    console.log(`Unhandled event type: ${event.type}`);
    return;
  }

  try {
    await handler(event.data);
    console.log(`Processed ${event.type}: ${event.id}`);
  } catch (error) {
    console.error(`Failed to process ${event.type}: ${event.id}`, error);
    throw error; // Rethrow to trigger retry
  }
}

Idempotency Handling

幂等性处理

typescript
import { Redis } from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

async function isEventProcessed(eventId: string): Promise<boolean> {
  const key = `posthog:event:${eventId}`;
  const exists = await redis.exists(key);
  return exists === 1;
}

async function markEventProcessed(eventId: string): Promise<void> {
  const key = `posthog:event:${eventId}`;
  await redis.set(key, '1', 'EX', 86400 * 7); // 7 days TTL
}
typescript
import { Redis } from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

async function isEventProcessed(eventId: string): Promise<boolean> {
  const key = `posthog:event:${eventId}`;
  const exists = await redis.exists(key);
  return exists === 1;
}

async function markEventProcessed(eventId: string): Promise<void> {
  const key = `posthog:event:${eventId}`;
  await redis.set(key, '1', 'EX', 86400 * 7); // 7 days TTL
}

Webhook Testing

Webhook测试

bash
undefined
bash
undefined

Use PostHog CLI to send test events

Use PostHog CLI to send test events

posthog webhooks trigger resource.created --url http://localhost:3000/webhooks/posthog
posthog webhooks trigger resource.created --url http://localhost:3000/webhooks/posthog

Or use webhook.site for debugging

Or use webhook.site for debugging

curl -X POST https://webhook.site/your-uuid
-H "Content-Type: application/json"
-d '{"type": "resource.created", "data": {}}'
undefined
curl -X POST https://webhook.site/your-uuid
-H "Content-Type: application/json"
-d '{"type": "resource.created", "data": {}}'
undefined

Instructions

操作步骤

Step 1: Register Webhook Endpoint

步骤1:注册Webhook端点

Configure your webhook URL in the PostHog dashboard.
在PostHog仪表盘中配置你的webhook URL。

Step 2: Implement Signature Verification

步骤2:实现签名验证

Use the signature verification code to validate incoming webhooks.
使用签名验证代码来验证传入的webhook。

Step 3: Handle Events

步骤3:处理事件

Implement handlers for each event type your application needs.
为应用所需的每种事件类型实现处理逻辑。

Step 4: Add Idempotency

步骤4:添加幂等性处理

Prevent duplicate processing with event ID tracking.
通过事件ID跟踪来防止重复处理。

Output

输出结果

  • Secure webhook endpoint
  • Signature validation enabled
  • Event handlers implemented
  • Replay attack protection active
  • 安全的webhook端点
  • 已启用签名验证
  • 已实现事件处理逻辑
  • 已激活重放攻击防护

Error Handling

错误处理

IssueCauseSolution
Invalid signatureWrong secretVerify webhook secret
Timestamp rejectedClock driftCheck server time sync
Duplicate eventsMissing idempotencyImplement event ID tracking
Handler timeoutSlow processingUse async queue
问题原因解决方案
无效签名密钥错误验证webhook密钥
时间戳被拒绝时钟偏移检查服务器时间同步
重复事件缺少幂等性处理实现事件ID跟踪
处理程序超时处理速度过慢使用异步队列

Examples

示例

Testing Webhooks Locally

本地测试Webhook

bash
undefined
bash
undefined

Use ngrok to expose local server

Use ngrok to expose local server

ngrok http 3000
ngrok http 3000

Send test webhook

Send test webhook

curl -X POST https://your-ngrok-url/webhooks/posthog
-H "Content-Type: application/json"
-d '{"type": "test", "data": {}}'
undefined
curl -X POST https://your-ngrok-url/webhooks/posthog
-H "Content-Type: application/json"
-d '{"type": "test", "data": {}}'
undefined

Resources

参考资源

Next Steps

下一步

For performance optimization, see
posthog-performance-tuning
.
如需性能优化,请查看
posthog-performance-tuning