关于 service-integration
service-integration 是将外部服务接入应用的技能,涵盖服务选择和验证。内容涉及常见服务类型,并说明错误处理、重试及不调用生产 API 的集成测试方法。
主要功能
- 01服务选择指导
- 02邮件、CMS、上传和分析集成模式
- 03Webhook 配置与验证
- 04重试和熔断器模式
- 05基于模拟的集成测试
- 06验证脚本
适用场景
- 01接入邮件服务商或 CMS
- 02集成文件上传或分析服务
- 03处理外部调用的重试与故障
- 04无需生产 API 测试 Webhook
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mgd34msu/ goodvibes-plugin --skill service-integration安装后在 Claude Code 中输入/service-integration确认已加载。
附带文件references/service-patterns.mdscripts/validate-services.sh
详细介绍
资源
scripts/
validate-services.sh
references/
service-patterns.md
服务集成实施
本技能将指导你完成将外部服务集成到应用程序的全过程,从服务选择到生产环境部署。它利用 GoodVibes 精准工具,实现类型安全、弹性强的服务集成,并提供适当的错误处理和测试。
何时使用此技能
在以下情况下使用此技能:
- 集成电子邮件服务提供商(Resend、SendGrid、Postmark)
- 设置内容管理系统(Sanity、Contentful、Payload、Strapi)
- 实现文件上传服务(UploadThing、Cloudinary、S3)
- 添加分析和跟踪(PostHog、Plausible、Google Analytics)
- 为第三方服务配置 webhook 端点
- 实现重试逻辑和断路器
- 在不调用生产 API 的情况下测试服务集成
前提条件
所需背景信息:
- 服务需求(电子邮件、CMS、上传、分析)
- 预期流量和规模要求
- 预算限制
- 可自托管服务与托管服务的偏好
工具:
- precision_grep,用于检测现有集成
- precision_read,用于分析服务配置
- precision_write,用于创建集成代码
- precision_exec,用于测试和验证
- discover,用于批量分析
阶段 1:发现——检测现有集成
添加新服务之前,先分析现有集成,以保持一致性。
步骤 1.1:检测服务 SDK 和模式
使用 discover 并行分析多个方面:
discover:
queries:
- id: email-sdks
type: grep
pattern: "(resend|sendgrid|postmark|nodemailer)"
glob: "package.json"
- id: cms-sdks
type: grep
pattern: "(@sanity|contentful|@payloadcms|@strapi)"
glob: "package.json"
- id: upload-sdks
type: grep
pattern: "(uploadthing|cloudinary|@aws-sdk/client-s3)"
glob: "package.json"
- id: analytics-sdks
type: grep
pattern: "(posthog-js|plausible|@vercel/analytics)"
glob: "package.json"
- id: api-keys-env
type: grep
pattern: "(RESEND_|SENDGRID_|SANITY_|CONTENTFUL_|UPLOADTHING_|CLOUDINARY_|AWS_|POSTHOG_)"
glob: ".env.example"
- id: service-clients
type: glob
patterns: ["src/lib/*client.ts", "src/services/**/*.ts", "lib/services/**/*.ts"]
- id: webhook-routes
type: glob
patterns: ["src/app/api/webhooks/**/*.ts", "pages/api/webhooks/**/*.ts"]
verbosity: files_only
步骤 1.2:分析服务客户端模式
阅读现有服务客户端,了解项目的模式:
precision_read:
files:
- path: "src/lib/email-client.ts"
extract: outline
- path: "src/services/upload.ts"
extract: symbols
output:
format: minimal
决策点: 新集成应采用现有模式。如果没有现成模式,请遵循以下实施指南。
阶段 2:服务选择
根据需求、规模和预算选择服务。
电子邮件服务决策树
用于事务性电子邮件:
- Resend - 开发体验(DX)最佳,支持 React Email,每天免费发送 100 封,50K 封每月 $20
- SendGrid - 企业级功能,每天免费发送 100 封,100K 封每月 $20
- Postmark - 专注于高送达率,10K 封每月 $15,无免费套餐
- AWS SES - 规模化后最便宜(每 1000 封 $0.10),需要更多设置
用于营销电子邮件:
- ConvertKit - 面向创作者,1K 名订阅者每月 $29
- Mailchimp - 一体化平台,500 名订阅者以内免费
- Loops - 对开发者友好,2K 名订阅者每月 $49
建议: 事务性电子邮件从 Resend 起步,达到每月 1M 封以上后迁移到 SES。
CMS 平台决策树
用于结构化内容(博客、文档):
- Sanity - 开发体验最佳,支持实时协作,免费套餐慷慨
- Contentful - 可用于企业级场景,GraphQL 功能强大,定价复杂
- Payload - 可自托管,完整支持 TypeScript,无供应商锁定
用于应用内容(产品、用户生成内容):
- Payload - 最适合复杂数据模型,内置身份验证
- Strapi - 插件生态丰富,可自托管
用于营销页面:
- Builder.io - 可视化编辑器,内置 A/B 测试
- Sanity - 对开发者友好,可通过 Sanity Studio 进行可视化编辑
建议: 内容密集型网站使用 Sanity,应用后端使用 Payload。
文件上传决策树
用于图片(头像、产品照片):
- UploadThing - 零配置,集成 Next.js,2GB 存储每月 $10
- Cloudinary - 支持图片转换,免费套餐提供 25GB 带宽
- Vercel Blob - 边缘网络,存储每 GB $0.15
用于大型文件(视频、文档):
- AWS S3 - 行业标准,存储每 GB $0.023,规模化后最便宜
- Cloudflare R2 - 兼容 S3,零出口流量费用,存储每 GB $0.015
- Backblaze B2 - 存储最便宜,每 GB $0.005
用于面向用户且需要病毒扫描的上传:
- UploadThing - 内置病毒扫描
- AWS S3 + Lambda - 使用 ClamAV 自行实现扫描
建议: 原型使用 UploadThing,生产规模场景使用 S3/R2。
分析决策树
用于产品分析:
- PostHog - 可自托管选项、会话回放、功能标记,免费套餐包含 1M 个事件
- Mixpanel - 以用户为中心的分析,免费套餐每月包含 20M 个事件
- Amplitude - 高级群组分析,免费套餐每月包含 10M 个事件
用于网站分析:
- Plausible - 注重隐私、符合 GDPR,10K 次页面浏览量每月 $9
- Umami - 可自托管、简单、开源
- Vercel Analytics - 专注于 Web Vitals,Vercel 项目可免费使用
建议: 产品应用使用 PostHog,营销网站使用 Plausible。
阶段 3:电子邮件集成
步骤 3.1:安装 Resend SDK
precision_exec:
commands:
- cmd: "npm install resend"
expect:
exit_code: 0
verbosity: minimal
步骤 3.2:创建电子邮件客户端
编写带有错误处理的类型安全电子邮件客户端:
precision_write:
files:
- path: "src/lib/email.ts"
mode: fail_if_exists
content: |
import type { ReactElement } from 'react';
import { Resend } from 'resend';
if (!process.env.RESEND_API_KEY) {
throw new Error('RESEND_API_KEY is required');
}
export const resend = new Resend(process.env.RESEND_API_KEY);
export interface SendEmailOptions {
to: string | string[];
subject: string;
html?: string;
react?: ReactElement;
from?: string;
}
export async function sendEmail(options: SendEmailOptions) {
const { to, subject, html, react, from = '[email protected]' } = options;
try {
const { data, error } = await resend.emails.send({
from,
to,
subject,
html,
react,
});
if (error) {
console.error('[Email] Send failed:', error);
throw new Error(`Email send failed: ${error.message}`);
}
console.log('[Email] Sent successfully:', data?.id);
return { success: true, id: data?.id };
} catch (error: unknown) {
console.error('[Email] Unexpected error:', error);
throw error;
}
}
verbosity: minimal
步骤 3.3:创建 React Email 模板
对于事务性电子邮件,使用 React Email 创建类型安全的模板:
precision_exec:
commands:
- cmd: "npm install react-email @react-email/components"
expect:
exit_code: 0
verbosity: minimal
precision_write:
files:
- path: "emails/welcome.tsx"
mode: fail_if_exists
content: |
import {
Body,
Button,
Container,
Head,
Heading,
Html,
Preview,
Text,
} from '@react-email/components';
interface WelcomeEmailProps {
userName: string;
loginUrl: string;
}
export default function WelcomeEmail({ userName, loginUrl }: WelcomeEmailProps) {
return (
<Html>
<Head />
<Preview>Welcome to our platform!</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Welcome, {userName}!</Heading>
<Text style={text}>
We're excited to have you on board. Click the button below to get started.
</Text>
<Button href={loginUrl} style={button}>
Get Started
</Button>
</Container>
</Body>
</Html>
);
}
const main = { backgroundColor: '#f6f9fc', fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif' };
const container = { margin: '0 auto', padding: '40px 20px' };
const h1 = { color: '#1f2937', fontSize: '24px', fontWeight: 'bold', marginBottom: '20px' };
const text = { color: '#4b5563', fontSize: '16px', lineHeight: '24px', marginBottom: '20px' };
const button = { backgroundColor: '#3b82f6', borderRadius: '6px', color: '#ffffff', display: 'inline-block', fontSize: '16px', fontWeight: '600', padding: '12px 24px', textDecoration: 'none' };
verbosity: minimal
步骤 3.4:记录环境变量
precision_write:
files:
- path: ".env.example"
mode: overwrite
content: |
# Email (Resend)
RESEND_API_KEY=your_resend_api_key_here
verbosity: minimal
阶段 4:CMS 集成
步骤 4.1:安装 Sanity SDK
precision_exec:
commands:
- cmd: "npm install @sanity/client @sanity/image-url"
expect:
exit_code: 0
verbosity: minimal
步骤 4.2:创建 Sanity 客户端
precision_write:
files:
- path: "src/lib/sanity.ts"
mode: fail_if_exists
content: |
import { createClient } from '@sanity/client';
import imageUrlBuilder from '@sanity/image-url';
import type { SanityImageSource } from '@sanity/image-url/lib/types/types';
if (!process.env.NEXT_PUBLIC_SANITY_PROJECT_ID) {
throw new Error('NEXT_PUBLIC_SANITY_PROJECT_ID is required');
}
if (!process.env.NEXT_PUBLIC_SANITY_DATASET) {
throw new Error('NEXT_PUBLIC_SANITY_DATASET is required');
}
export const sanityClient = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
apiVersion: '2026-01-01', // Update to current API version date
useCdn: process.env.NODE_ENV === 'production',
});
const builder = imageUrlBuilder(sanityClient);
// Import from @sanity/image-url
export function urlForImage(source: SanityImageSource) {
return builder.image(source).auto('format').fit('max');
}
// Type-safe query helper
export async function sanityFetch<T>(query: string, params?: Record<string, unknown>): Promise<T> {
try {
const result = await sanityClient.fetch<T>(query, params);
return result;
} catch (error: unknown) {
console.error('[Sanity] Query failed:', error);
throw new Error('Failed to fetch from Sanity');
}
}
verbosity: minimal
步骤 4.3:设置 Webhook 端点
如需实时更新内容,请实现 Webhook 处理程序:
precision_write:
files:
- path: "src/app/api/webhooks/sanity/route.ts"
mode: fail_if_exists
content: |
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
import { timingSafeEqual } from 'crypto';
export async function POST(request: NextRequest) {
const signature = request.headers.get('sanity-webhook-signature');
// Verify webhook signature
const secret = process.env.SANITY_WEBHOOK_SECRET;
if (!secret) {
return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
}
const expected = Buffer.from(secret);
const received = Buffer.from(signature || '');
if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
try {
const body = await request.json();
const { _type } = body;
// Revalidate cache based on content type
if (_type === 'post') {
revalidateTag('posts');
} else if (_type === 'page') {
revalidateTag('pages');
}
console.log('[Webhook] Sanity content updated:', _type); // Note: Use structured logger in production
return NextResponse.json({ revalidated: true });
} catch (error: unknown) {
console.error('[Webhook] Failed to process Sanity webhook:', error);
return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 });
}
}
verbosity: minimal
阶段 5:文件上传集成
步骤 5.1:安装 UploadThing SDK
precision_exec:
commands:
- cmd: "npm install uploadthing @uploadthing/react"
expect:
exit_code: 0
verbosity: minimal
步骤 5.2:创建上传路由器
precision_write:
files:
- path: "src/app/api/uploadthing/core.ts"
mode: fail_if_exists
content: |
import { createUploadthing, type FileRouter } from 'uploadthing/next';
const f = createUploadthing();
export const ourFileRouter = {
imageUploader: f({ image: { maxFileSize: '4MB', maxFileCount: 4 } })
.middleware(async ({ req }) => {
// Authenticate user (placeholder imports shown for context)
// In real code: import { getUserFromRequest } from '@/lib/auth';
const user = await getUserFromRequest(req); // Import from your auth module
if (!user) throw new Error('Unauthorized');
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log('[Upload] Complete:', file.url);
// Save to database (Assumes Prisma client or similar ORM)
await db.image.create({
data: {
url: file.url,
userId: metadata.userId,
},
});
return { url: file.url };
}),
pdfUploader: f({ pdf: { maxFileSize: '16MB' } })
.middleware(async ({ req }) => {
const user = await getUserFromRequest(req);
if (!user) throw new Error('Unauthorized');
return { userId: user.id };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log('[Upload] PDF complete:', file.url);
return { url: file.url };
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;
verbosity: minimal
步骤 5.3:替代方案 - S3 预签名 URL
如需更多控制,请使用带有预签名 URL 的 S3:
precision_exec:
commands:
- cmd: "npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
expect:
exit_code: 0
verbosity: minimal
precision_write:
files:
- path: "src/lib/s3.ts"
mode: fail_if_exists
content: |
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
// Validate S3 configuration
if (!process.env.AWS_REGION || !process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY || !process.env.AWS_S3_BUCKET) {
throw new Error('Missing required AWS S3 environment variables');
}
const s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
export async function getUploadUrl(key: string, contentType: string) {
const command = new PutObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: key,
ContentType: contentType,
});
// URL expires in 5 minutes
const url = await getSignedUrl(s3Client, command, { expiresIn: 300 });
return url;
}
verbosity: minimal
阶段 6:分析集成
步骤 6.1:安装 PostHog SDK
precision_exec:
commands:
- cmd: "npm install posthog-js"
expect:
exit_code: 0
verbosity: minimal
步骤 6.2:创建分析提供程序
precision_write:
files:
- path: "src/providers/analytics.tsx"
mode: fail_if_exists
content: |
'use client';
import { useEffect } from 'react';
import type { ReactNode } from 'react';
import posthog from 'posthog-js';
export function AnalyticsProvider({ children }: { children: ReactNode }) {
useEffect(() => {
if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
loaded: (posthog) => {
if (process.env.NODE_ENV === 'development') {
posthog.opt_out_capturing(); // Disabled in dev to avoid polluting analytics (re-enable for testing with posthog.opt_in_capturing())
}
},
});
}
}, []);
return <>{children}</>;
}
// Helper for tracking events
export function trackEvent(eventName: string, properties?: Record<string, unknown>) {
if (typeof window !== 'undefined') {
posthog.capture(eventName, properties);
}
}
verbosity: minimal
阶段 7:错误处理模式
步骤 7.1:实现指数退避
对于有速率限制的服务,请实现重试逻辑:
precision_write:
files:
- path: "src/lib/retry.ts"
mode: fail_if_exists
content: |
export interface RetryOptions {
maxAttempts?: number;
initialDelayMs?: number;
maxDelayMs?: number;
backoffMultiplier?: number;
}
export async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const {
maxAttempts = 3,
initialDelayMs = 1000,
maxDelayMs = 10000,
backoffMultiplier = 2,
} = options;
let lastError: Error = new Error('All retry attempts failed');
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error: unknown) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt === maxAttempts) {
break;
}
const delayMs = Math.min(
initialDelayMs * Math.pow(backoffMultiplier, attempt - 1),
maxDelayMs
);
console.log(`[Retry] Attempt ${attempt}/${maxAttempts} failed, retrying in ${delayMs}ms`); // Note: Use structured logger in production
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
throw lastError;
}
verbosity: minimal
步骤 7.2:实现熔断器
使用熔断器防止故障级联:
precision_write:
files:
- path: "src/lib/circuit-breaker.ts"
mode: fail_if_exists
content: |
export class CircuitBreaker {
private failureCount = 0;
private lastFailureTime: number | null = null;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private readonly failureThreshold = 5,
private readonly resetTimeoutMs = 60000
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - (this.lastFailureTime ?? 0) > this.resetTimeoutMs) {
this.state = 'half-open';
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error: unknown) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failureCount = 0;
this.state = 'closed';
}
private onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'open';
console.error('[CircuitBreaker] Circuit opened due to repeated failures');
}
}
}
verbosity: minimal
阶段 8:测试服务集成
步骤 8.1:模拟服务响应
创建模拟实现以便测试:
precision_write:
files:
- path: "src/lib/__mocks__/email.ts"
mode: fail_if_exists
content: |
import { SendEmailOptions } from '../email';
const sentEmails: Array<SendEmailOptions & { id: string }> = [];
export async function sendEmail(options: SendEmailOptions) {
const id = `mock-${Date.now()}`;
sentEmails.push({ ...options, id });
return { success: true, id };
}
export function getSentEmails() {
return sentEmails;
}
export function clearSentEmails() {
sentEmails.length = 0;
}
verbosity: minimal
步骤 8.2:实现 Webhook 测试
在本地测试 Webhook 处理程序:
precision_write:
files:
- path: "scripts/test-webhook.sh"
mode: fail_if_exists
content: |
#!/usr/bin/env bash
set -euo pipefail
# Requires: bash 4+
# Test Sanity webhook locally
echo "[INFO] Testing Sanity webhook..."
if curl -X POST http://localhost:3000/api/webhooks/sanity \
-H "Content-Type: application/json" \
-H "sanity-webhook-signature: $SANITY_WEBHOOK_SECRET" \
-d '{
"_type": "post",
"_id": "test-123",
"title": "Test Post"
}'; then
echo "[PASS] Webhook test successful"
else
echo "[FAIL] Webhook test failed"
exit 1
fi
verbosity: minimal
阶段 9:验证
运行验证脚本,确保服务已正确集成:
precision_exec:
commands:
- cmd: "bash plugins/goodvibes/skills/outcome/service-integration/scripts/validate-services.sh ."
expect:
exit_code: 0
verbosity: standard
常见模式
环境变量验证
始终在启动时验证必需的环境变量:
const requiredEnvVars = ['RESEND_API_KEY', 'SANITY_PROJECT_ID'];
for (const envVar of requiredEnvVars) {
if (!process.env[envVar]) {
throw new Error(`Missing required environment variable: ${envVar}`);
}
}
对外发请求进行速率限制
实现令牌桶速率限制:
export class RateLimiter {
private tokens: number;
private lastRefill: number;
constructor(
private readonly maxTokens: number,
private readonly refillRatePerSecond: number
) {
this.tokens = maxTokens;
this.lastRefill = Date.now();
}
async acquire(): Promise<void> {
this.refill();
if (this.tokens < 1) {
const waitMs = (1 - this.tokens) * (1000 / this.refillRatePerSecond);
await new Promise(resolve => setTimeout(resolve, waitMs));
this.refill();
}
this.tokens -= 1;
}
private refill() {
const now = Date.now();
const elapsedSeconds = (now - this.lastRefill) / 1000;
const tokensToAdd = elapsedSeconds * this.refillRatePerSecond;
this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
应避免的反模式
1. 硬编码 API 密钥
错误:
const resend = new Resend('re_abc123');
正确:
if (!process.env.RESEND_API_KEY) {
throw new Error('RESEND_API_KEY is required');
}
const resend = new Resend(process.env.RESEND_API_KEY);
2. 未处理错误
错误:
const result = await resend.emails.send(options);
return result.data;
正确:
const { data, error } = await resend.emails.send(options);
if (error) {
console.error('[Email] Send failed:', error);
throw new Error(`Email send failed: ${error.message}`);
}
return data;
3. 同步外部调用
错误:
// Blocking user request
await sendEmail({ to: user.email, subject: 'Welcome' });
return res.json({ success: true });
可接受(适用于简单情况):
// Send email async
// Note: Fire-and-forget is only suitable for non-critical operations.
// For production: use a queue-based approach with retries (see BEST example below).
sendEmail({ to: user.email, subject: 'Welcome' })
.catch(err => console.error('[Email] Failed:', err));
return res.json({ success: true });
最佳(可用于生产环境):
// Queue-based approach with retries
await emailQueue.add('welcome-email', {
to: user.email,
subject: 'Welcome'
});
return res.json({ success: true });
4. 未验证 Webhook
错误:
export async function POST(request: NextRequest) {
const body = await request.json();
// Process without verification
}
正确:
import { timingSafeEqual } from 'crypto';
export async function POST(request: NextRequest) {
const signature = request.headers.get('webhook-signature');
// Validate webhook secret
const secret = process.env.WEBHOOK_SECRET;
if (!secret) {
return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
}
const expected = Buffer.from(secret);
const received = Buffer.from(signature || '');
if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const body = await request.json();
// Process
}
故障排除
电子邮件未发送
- 验证是否已设置 API 密钥:
printf "%s\n" "${RESEND_API_KEY:0:5}..."(用于显示前 5 个字符的 bash 参数展开) - 在提供商控制面板中检查 API 密钥权限
- 验证发件人域名是否已通过验证
- 在提供商日志中检查速率限制
- 启用调试日志:
resend.setDebug(true)
CMS 内容未更新
- 验证 Webhook 端点是否可从公网访问
- 检查 Webhook 密钥是否匹配
- 使用 ngrok 在本地测试 Webhook
- 验证缓存重新验证是否正常工作
- 检查 CMS Webhook 日志
文件上传失败
- 验证文件大小是否在限制范围内
- 检查 CORS 配置
- 验证身份验证中间件
- 使用较小的文件进行测试
- 检查 S3 存储桶权限(如果使用 S3)
分析事件未被跟踪
- 验证是否已设置 API 密钥
- 检查广告拦截器是否阻止了请求
- 验证开发环境中是否已关闭退出跟踪选项
- 检查浏览器控制台中的错误
- 使用 PostHog 调试模式进行测试
相关技能
- api-design - 类型安全的 API 层模式
- authentication - 安全的服务访问模式
- testing-strategy - 测试服务集成
成功标准
- 已安装服务 SDK 并创建客户端
- 已在 .env.example 中记录环境变量
- 已实现带重试的错误处理
- Webhook 端点已进行签名验证
- 已配置出站请求的速率限制
- 已提供用于测试的模拟实现
- 源代码中没有硬编码的 API 密钥
- 所有验证检查均已通过