Skip to content

Commit 36d4b0f

Browse files
devkiransteven-tey
andauthored
Request logs (dubinc#3719)
Co-authored-by: Steven Tey <stevensteel97@gmail.com>
1 parent 6fd111b commit 36d4b0f

54 files changed

Lines changed: 2212 additions & 115 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/web/app/(ee)/api/appsflyer/webhook/route.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { captureWebhookLog } from "@/lib/api-logs/capture-webhook-log";
12
import { trackLead } from "@/lib/api/conversions/track-lead";
23
import { trackSale } from "@/lib/api/conversions/track-sale";
34
import { isLocalDev } from "@/lib/api/environment";
@@ -10,7 +11,9 @@ import { isIpInRange } from "@/lib/middleware/utils/is-ip-in-range";
1011
import { trackLeadRequestSchema } from "@/lib/zod/schemas/leads";
1112
import { trackSaleRequestSchema } from "@/lib/zod/schemas/sales";
1213
import { prisma } from "@dub/prisma";
14+
import { Project } from "@dub/prisma/client";
1315
import { APPSFLYER_INTEGRATION_ID, getSearchParams } from "@dub/utils";
16+
import { waitUntil } from "@vercel/functions";
1417
import { NextResponse } from "next/server";
1518
import * as z from "zod/v4";
1619

@@ -22,6 +25,14 @@ const querySchema = z.object({
2225

2326
// GET /api/appsflyer/webhook – listen to Postback events from AppsFlyer
2427
export const GET = withAxiom(async (req) => {
28+
const startTime = Date.now();
29+
let response = "OK";
30+
let queryParams: Record<string, string> | null = null;
31+
let workspace: Pick<
32+
Project,
33+
"id" | "stripeConnectId" | "webhookEnabled"
34+
> | null = null;
35+
2536
try {
2637
if (!isLocalDev) {
2738
const ip = await getIP();
@@ -37,7 +48,7 @@ export const GET = withAxiom(async (req) => {
3748
}
3849
}
3950

40-
const queryParams = getSearchParams(req.url);
51+
queryParams = getSearchParams(req.url);
4152

4253
const { appId, partnerEventId } = querySchema.parse(queryParams);
4354

@@ -72,6 +83,8 @@ export const GET = withAxiom(async (req) => {
7283
});
7384
}
7485

86+
workspace = installation.project;
87+
7588
// Track lead event
7689
if (partnerEventId === "lead") {
7790
const {
@@ -93,15 +106,15 @@ export const GET = withAxiom(async (req) => {
93106
eventQuantity: undefined,
94107
mode: undefined,
95108
metadata: null,
96-
workspace: installation.project,
109+
workspace,
97110
rawBody: queryParams,
98111
});
99112

100-
return NextResponse.json("Lead event tracked successfully.");
113+
response = "Lead event tracked successfully.";
101114
}
102115

103116
// Track sale event
104-
if (partnerEventId === "sale") {
117+
else if (partnerEventId === "sale") {
105118
const amountInCents = appsflyerAmountToDubCents(queryParams.amount);
106119
const { eventName, customerExternalId, amount, currency, invoiceId } =
107120
trackSaleRequestSchema.parse({
@@ -118,16 +131,46 @@ export const GET = withAxiom(async (req) => {
118131
invoiceId,
119132
leadEventName: undefined,
120133
metadata: null,
121-
workspace: installation.project,
134+
workspace,
122135
rawBody: queryParams,
123136
});
124137

125-
return NextResponse.json("Sale event tracked successfully.");
138+
response = "Sale event tracked successfully.";
126139
}
127140

128-
return NextResponse.json("OK");
141+
waitUntil(
142+
captureWebhookLog({
143+
workspaceId: workspace.id,
144+
method: req.method,
145+
path: "/api/appsflyer/webhook",
146+
statusCode: 200,
147+
duration: Date.now() - startTime,
148+
requestBody: queryParams,
149+
responseBody: response,
150+
userAgent: req.headers.get("user-agent"),
151+
}),
152+
);
153+
154+
return NextResponse.json(response);
129155
} catch (error) {
130-
return handleAndReturnErrorResponse(error);
156+
const errorResponse = handleAndReturnErrorResponse(error);
157+
158+
if (workspace) {
159+
waitUntil(
160+
captureWebhookLog({
161+
workspaceId: workspace.id,
162+
method: req.method,
163+
path: "/api/appsflyer/webhook",
164+
statusCode: errorResponse.status,
165+
duration: Date.now() - startTime,
166+
requestBody: queryParams,
167+
responseBody: errorResponse,
168+
userAgent: req.headers.get("user-agent"),
169+
}),
170+
);
171+
}
172+
173+
return errorResponse;
131174
}
132175
});
133176

apps/web/app/(ee)/api/stripe/integration/webhook/account-application-deauthorized.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export async function accountApplicationDeauthorized(
1111
const stripeAccountId = event.account;
1212

1313
if (mode === "test") {
14-
return `Stripe Connect account ${stripeAccountId} deauthorized in test mode. Skipping...`;
14+
return {
15+
response: `Stripe Connect account ${stripeAccountId} deauthorized in test mode. Skipping...`,
16+
};
1517
}
1618

1719
const workspace = await prisma.project.findUnique({
@@ -24,7 +26,9 @@ export async function accountApplicationDeauthorized(
2426
});
2527

2628
if (!workspace) {
27-
return `Stripe Connect account ${stripeAccountId} deauthorized.`;
29+
return {
30+
response: `Workspace not found for Stripe account ${stripeAccountId}, skipping...`,
31+
};
2832
}
2933

3034
await prisma.project.update({
@@ -46,5 +50,8 @@ export async function accountApplicationDeauthorized(
4650
},
4751
});
4852

49-
return `Stripe Connect account ${stripeAccountId} deauthorized for workspace ${workspace.id}`;
53+
return {
54+
response: `Stripe Connect account ${stripeAccountId} deauthorized for workspace ${workspace.id}`,
55+
workspaceId: workspace.id,
56+
};
5057
}

apps/web/app/(ee)/api/stripe/integration/webhook/charge-refunded.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ export async function chargeRefunded(event: Stripe.Event, mode: StripeMode) {
3131
invoicePayments.data.length > 0 ? invoicePayments.data[0] : null;
3232

3333
if (!invoicePayment || !invoicePayment.invoice) {
34-
return `Charge ${charge.id} has no invoice, skipping...`;
34+
return {
35+
response: `Charge ${charge.id} has no invoice, skipping...`,
36+
};
3537
}
3638

3739
const workspace = await prisma.project.findUnique({
@@ -45,11 +47,18 @@ export async function chargeRefunded(event: Stripe.Event, mode: StripeMode) {
4547
});
4648

4749
if (!workspace) {
48-
return `Workspace not found for stripe account ${stripeAccountId}`;
50+
return {
51+
response: `Workspace not found for Stripe account ${stripeAccountId}, skipping...`,
52+
};
4953
}
5054

55+
const workspaceId = workspace.id;
56+
5157
if (!workspace.programs.length) {
52-
return `Workspace ${workspace.id} for stripe account ${stripeAccountId} has no programs, skipping...`;
58+
return {
59+
response: `Workspace ${workspaceId} for stripe account ${stripeAccountId} has no programs, skipping...`,
60+
workspaceId,
61+
};
5362
}
5463

5564
const commission = await prisma.commission.findUnique({
@@ -71,11 +80,17 @@ export async function chargeRefunded(event: Stripe.Event, mode: StripeMode) {
7180
});
7281

7382
if (!commission) {
74-
return `Commission not found for invoice ${invoicePayment.invoice}`;
83+
return {
84+
response: `Commission not found for invoice ${invoicePayment.invoice}`,
85+
workspaceId,
86+
};
7587
}
7688

7789
if (commission.status === "paid") {
78-
return `Commission ${commission.id} is already paid, skipping...`;
90+
return {
91+
response: `Commission ${commission.id} is already paid, skipping...`,
92+
workspaceId,
93+
};
7994
}
8095

8196
// if the commission is processed and has a payout, we need to update the payout total
@@ -122,5 +137,8 @@ export async function chargeRefunded(event: Stripe.Event, mode: StripeMode) {
122137
newStatus: "refunded",
123138
});
124139

125-
return `Commission ${commission.id} updated to status "refunded"`;
140+
return {
141+
response: `Commission ${commission.id} updated to status "refunded"`,
142+
workspaceId,
143+
};
126144
}

apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ import { Customer, Project } from "@dub/prisma/client";
2727
import { COUNTRIES_TO_CONTINENTS, nanoid, pick } from "@dub/utils";
2828
import { waitUntil } from "@vercel/functions";
2929
import type Stripe from "stripe";
30+
import { getCheckoutSessionProductId } from "./utils/get-checkout-session-product-id";
3031
import { getConnectedCustomer } from "./utils/get-connected-customer";
3132
import { getPromotionCode } from "./utils/get-promotion-code";
32-
import { getCheckoutSessionProductId } from "./utils/get-checkout-session-product-id";
3333
import { updateCustomerWithStripeCustomerId } from "./utils/update-customer-with-stripe-customer-id";
3434

3535
// Handle event "checkout.session.completed"
@@ -70,7 +70,9 @@ export async function checkoutSessionCompleted(
7070
});
7171

7272
if (!workspace) {
73-
return `Workspace with stripeConnectId ${stripeAccountId} not found, skipping...`;
73+
return {
74+
response: `Workspace not found for Stripe account ${stripeAccountId}, skipping...`,
75+
};
7476
}
7577

7678
/*
@@ -85,7 +87,10 @@ export async function checkoutSessionCompleted(
8587
clickEvent = await getClickEvent({ clickId: dubClickId });
8688

8789
if (!clickEvent) {
88-
return `Click event with dub_id ${dubClickId} not found, skipping...`;
90+
return {
91+
response: `Click event with dub_id ${dubClickId} not found, skipping...`,
92+
workspaceId: workspace.id,
93+
};
8994
}
9095

9196
existingCustomer = await prisma.customer.findFirst({
@@ -199,10 +204,16 @@ export async function checkoutSessionCompleted(
199204
if (promoCodeResponse) {
200205
({ linkId, customer, clickEvent, leadEvent } = promoCodeResponse);
201206
} else {
202-
return `Failed to attribute via promotion code ${promotionCodeId}, skipping...`;
207+
return {
208+
response: `Failed to attribute via promotion code ${promotionCodeId}, skipping...`,
209+
workspaceId: workspace.id,
210+
};
203211
}
204212
} else {
205-
return `dubCustomerExternalId was provided but customer with dubCustomerExternalId ${dubCustomerExternalId} not found on Dub, skipping...`;
213+
return {
214+
response: `dubCustomerExternalId was provided but customer with dubCustomerExternalId ${dubCustomerExternalId} not found on Dub, skipping...`,
215+
workspaceId: workspace.id,
216+
};
206217
}
207218
}
208219
} else {
@@ -247,7 +258,10 @@ export async function checkoutSessionCompleted(
247258
stripeCustomerId,
248259
});
249260
if (!customer) {
250-
return `dubCustomerExternalId was found on the connected customer ${stripeCustomerId} but customer with dubCustomerExternalId ${dubCustomerExternalId} not found on Dub, skipping...`;
261+
return {
262+
response: `dubCustomerExternalId was found on the connected customer ${stripeCustomerId} but customer with dubCustomerExternalId ${dubCustomerExternalId} not found on Dub, skipping...`,
263+
workspaceId: workspace.id,
264+
};
251265
}
252266
} else if (promotionCodeId) {
253267
const promoCodeResponse = await attributeViaPromoCode({
@@ -260,10 +274,16 @@ export async function checkoutSessionCompleted(
260274
if (promoCodeResponse) {
261275
({ linkId, customer, clickEvent, leadEvent } = promoCodeResponse);
262276
} else {
263-
return `Failed to attribute via promotion code ${promotionCodeId}, skipping...`;
277+
return {
278+
response: `Failed to attribute via promotion code ${promotionCodeId}, skipping...`,
279+
workspaceId: workspace.id,
280+
};
264281
}
265282
} else {
266-
return `dubCustomerExternalId not found in Stripe checkout session metadata (nor is it available on the connected customer ${stripeCustomerId}), client_reference_id is not a dub_id, and promotion code is not provided, skipping...`;
283+
return {
284+
response: `dubCustomerExternalId not found in Stripe checkout session metadata (nor is it available on the connected customer ${stripeCustomerId}), client_reference_id is not a dub_id, and promotion code is not provided, skipping...`,
285+
workspaceId: workspace.id,
286+
};
267287
}
268288
}
269289
}
@@ -272,7 +292,10 @@ export async function checkoutSessionCompleted(
272292
if (!leadEvent) {
273293
const leadEventData = await getLeadEvent({ customerId: customer.id });
274294
if (!leadEventData) {
275-
return `No lead event found for customer ${customer.id}, skipping...`;
295+
return {
296+
response: `No lead event found for customer ${customer.id}, skipping...`,
297+
workspaceId: workspace.id,
298+
};
276299
}
277300
leadEvent = {
278301
...leadEventData,
@@ -281,23 +304,35 @@ export async function checkoutSessionCompleted(
281304
linkId = leadEvent.link_id;
282305
}
283306
} else {
284-
return "No stripeCustomerId or dubCustomerExternalId found in Stripe checkout session metadata, skipping...";
307+
return {
308+
response: `No stripeCustomerId or dubCustomerExternalId found in Stripe checkout session metadata, skipping...`,
309+
workspaceId: workspace.id,
310+
};
285311
}
286312

287313
let chargeAmountTotal =
288314
(charge.amount_total ?? 0) - (charge.total_details?.amount_tax ?? 0);
289315

290316
// should never be below 0, but just in case
291317
if (chargeAmountTotal <= 0) {
292-
return `Checkout session completed for Stripe customer ${stripeCustomerId} but amount is 0, skipping...`;
318+
return {
319+
response: `Checkout session completed for Stripe customer ${stripeCustomerId} but amount is 0, skipping...`,
320+
workspaceId: workspace.id,
321+
};
293322
}
294323

295324
if (charge.mode === "setup") {
296-
return `Checkout session completed for Stripe customer ${stripeCustomerId} but mode is "setup", skipping...`;
325+
return {
326+
response: `Checkout session completed for Stripe customer ${stripeCustomerId} but mode is "setup", skipping...`,
327+
workspaceId: workspace.id,
328+
};
297329
}
298330

299331
if (charge.payment_status !== "paid") {
300-
return `Checkout session completed for Stripe customer ${stripeCustomerId} but payment_status is not "paid", skipping...`;
332+
return {
333+
response: `Checkout session completed for Stripe customer ${stripeCustomerId} but payment_status is not "paid", skipping...`,
334+
workspaceId: workspace.id,
335+
};
301336
}
302337

303338
if (invoiceId) {
@@ -326,7 +361,10 @@ export async function checkoutSessionCompleted(
326361
"[Stripe Webhook] Skipping already processed invoice.",
327362
invoiceId,
328363
);
329-
return `Invoice with ID ${invoiceId} already processed, skipping...`;
364+
return {
365+
response: `Invoice with ID ${invoiceId} already processed, skipping...`,
366+
workspaceId: workspace.id,
367+
};
330368
}
331369
}
332370

@@ -549,7 +587,10 @@ export async function checkoutSessionCompleted(
549587
]),
550588
);
551589

552-
return `Checkout session completed for customer with external ID ${dubCustomerExternalId} and invoice ID ${invoiceId}`;
590+
return {
591+
response: `Checkout session completed for customer with external ID ${dubCustomerExternalId} and invoice ID ${invoiceId}`,
592+
workspaceId: workspace.id,
593+
};
553594
}
554595

555596
async function attributeViaPromoCode({

0 commit comments

Comments
 (0)