|
1 | 1 | import { Database } from "../db/database.types"; |
2 | 2 | import { createHmac } from "crypto"; |
3 | | -import { PromiseGenericResult, ok } from "../../packages/common/result"; |
| 3 | +import { PromiseGenericResult, ok, err } from "../../packages/common/result"; |
4 | 4 | import { WebhookConfig } from "../shared/types"; |
| 5 | +import { randomUUID } from "crypto"; |
5 | 6 |
|
6 | 7 | export type WebhookPayload = { |
7 | 8 | payload: { |
@@ -168,3 +169,173 @@ export async function sendToWebhook( |
168 | 169 |
|
169 | 170 | return ok(`Successfully sent to webhook`); |
170 | 171 | } |
| 172 | + |
| 173 | +// Generate mock data for testing webhooks |
| 174 | +function generateMockWebhookData(): WebhookData { |
| 175 | + const requestId = randomUUID(); |
| 176 | + const timestamp = Math.floor(Date.now() / 1000); |
| 177 | + |
| 178 | + // Mock OpenAI chat completion request |
| 179 | + const requestBody = { |
| 180 | + model: "gpt-4o", |
| 181 | + messages: [ |
| 182 | + { |
| 183 | + role: "user", |
| 184 | + content: "test message" |
| 185 | + } |
| 186 | + ] |
| 187 | + }; |
| 188 | + |
| 189 | + // Mock OpenAI chat completion response |
| 190 | + const responseBody = { |
| 191 | + id: `chatcmpl-${randomUUID().substring(0, 29)}`, |
| 192 | + object: "chat.completion", |
| 193 | + created: timestamp, |
| 194 | + model: "gpt-4o-2024-08-06", |
| 195 | + choices: [ |
| 196 | + { |
| 197 | + index: 0, |
| 198 | + message: { |
| 199 | + role: "assistant", |
| 200 | + content: "Hey! Not much, just here to help you out. What's up with you?", |
| 201 | + refusal: null, |
| 202 | + annotations: [] |
| 203 | + }, |
| 204 | + logprobs: null, |
| 205 | + finish_reason: "stop" |
| 206 | + } |
| 207 | + ], |
| 208 | + usage: { |
| 209 | + prompt_tokens: 13, |
| 210 | + completion_tokens: 17, |
| 211 | + total_tokens: 30, |
| 212 | + prompt_tokens_details: { |
| 213 | + cached_tokens: 0, |
| 214 | + audio_tokens: 0 |
| 215 | + }, |
| 216 | + completion_tokens_details: { |
| 217 | + reasoning_tokens: 0, |
| 218 | + audio_tokens: 0, |
| 219 | + accepted_prediction_tokens: 0, |
| 220 | + rejected_prediction_tokens: 0 |
| 221 | + } |
| 222 | + }, |
| 223 | + service_tier: "default", |
| 224 | + system_fingerprint: `fp_${randomUUID().substring(0, 14)}` |
| 225 | + }; |
| 226 | + |
| 227 | + // Generate mock S3 URL with AWS signature |
| 228 | + const s3Url = `https://s3.us-west-2.amazonaws.com/request-response-storage/organizations/${randomUUID()}/requests/${requestId}/request_response_body?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=MOCKAWSCREDENTIAL%2F${new Date().toISOString().split('T')[0].replace(/-/g, '')}%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=${new Date().toISOString().replace(/[:-]/g, '').split('.')[0]}Z&X-Amz-Expires=86400&X-Amz-Security-Token=MockSecurityToken&X-Amz-Signature=mocksignature123456789&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject`; |
| 229 | + |
| 230 | + return { |
| 231 | + request_id: requestId, |
| 232 | + request_body: JSON.stringify(requestBody), |
| 233 | + response_body: JSON.stringify(responseBody), |
| 234 | + request_response_url: s3Url, |
| 235 | + model: "gpt-4o-2024-08-06", |
| 236 | + provider: "OPENAI", |
| 237 | + metadata: { |
| 238 | + cost: 0.00020250000000000002, |
| 239 | + promptTokens: 13, |
| 240 | + completionTokens: 17, |
| 241 | + totalTokens: 30, |
| 242 | + latencyMs: 930 |
| 243 | + } |
| 244 | + }; |
| 245 | +} |
| 246 | + |
| 247 | +// Test webhook sender without delay |
| 248 | +export async function sendTestWebhook( |
| 249 | + webhook: { |
| 250 | + id: string; |
| 251 | + destination: string; |
| 252 | + config: string | any; |
| 253 | + hmac_key: string; |
| 254 | + } |
| 255 | +): PromiseGenericResult<string> { |
| 256 | + try { |
| 257 | + const hmacKey = webhook.hmac_key ?? ""; |
| 258 | + let config: WebhookConfig; |
| 259 | + |
| 260 | + // Handle both string and object configs (database might return either) |
| 261 | + if (typeof webhook.config === 'string') { |
| 262 | + try { |
| 263 | + config = (JSON.parse(webhook.config) as WebhookConfig) || {}; |
| 264 | + } catch (parseError) { |
| 265 | + return err(`Failed to parse webhook config: ${parseError instanceof Error ? parseError.message : 'Invalid JSON'}`); |
| 266 | + } |
| 267 | + } else { |
| 268 | + config = (webhook.config as WebhookConfig) || {}; |
| 269 | + } |
| 270 | + |
| 271 | + const includeData = config.includeData !== false; |
| 272 | + |
| 273 | + if ( |
| 274 | + !webhook.destination || |
| 275 | + typeof webhook.destination !== "string" || |
| 276 | + !webhook.destination.startsWith("https://") |
| 277 | + ) { |
| 278 | + return err(`Invalid destination URL. Must start with https://`); |
| 279 | + } |
| 280 | + |
| 281 | + // Generate mock data |
| 282 | + const mockData = generateMockWebhookData(); |
| 283 | + |
| 284 | + // Create webhook payload based on includeData setting |
| 285 | + const webHookPayloadObj: WebhookData = { |
| 286 | + request_id: mockData.request_id, |
| 287 | + request_body: mockData.request_body, |
| 288 | + response_body: mockData.response_body, |
| 289 | + }; |
| 290 | + |
| 291 | + // Add additional data if includeData is true |
| 292 | + if (includeData) { |
| 293 | + webHookPayloadObj.request_response_url = mockData.request_response_url; |
| 294 | + webHookPayloadObj.model = mockData.model; |
| 295 | + webHookPayloadObj.provider = mockData.provider; |
| 296 | + webHookPayloadObj.metadata = mockData.metadata; |
| 297 | + } |
| 298 | + |
| 299 | + const webHookPayload = JSON.stringify(webHookPayloadObj); |
| 300 | + |
| 301 | + // Generate HMAC signature |
| 302 | + const hmac = createHmac("sha256", hmacKey); |
| 303 | + hmac.update(webHookPayload); |
| 304 | + const hash = hmac.digest("hex"); |
| 305 | + |
| 306 | + // Set a shorter timeout for test webhooks (10 seconds) |
| 307 | + const controller = new AbortController(); |
| 308 | + const timeoutId = setTimeout(() => controller.abort(), 10 * 1000); |
| 309 | + |
| 310 | + try { |
| 311 | + const response = await fetch(webhook.destination, { |
| 312 | + method: "POST", |
| 313 | + body: webHookPayload, |
| 314 | + headers: { |
| 315 | + "Content-Type": "application/json", |
| 316 | + "Helicone-Signature": hash, |
| 317 | + }, |
| 318 | + signal: controller.signal, |
| 319 | + }); |
| 320 | + |
| 321 | + if (!response.ok) { |
| 322 | + throw new Error( |
| 323 | + `Webhook test failed with status ${response.status}: ${response.statusText}` |
| 324 | + ); |
| 325 | + } |
| 326 | + } finally { |
| 327 | + clearTimeout(timeoutId); |
| 328 | + } |
| 329 | + } catch (error: unknown) { |
| 330 | + if (error instanceof Error && error.name === "AbortError") { |
| 331 | + return err("Test webhook request timed out after 10 seconds"); |
| 332 | + } |
| 333 | + return err( |
| 334 | + `Test webhook failed: ${ |
| 335 | + error instanceof Error ? error.message : String(error) |
| 336 | + }` |
| 337 | + ); |
| 338 | + } |
| 339 | + |
| 340 | + return ok(`Test webhook sent successfully`); |
| 341 | +} |
0 commit comments