feat(donate): add PostHog analytics events
Track donation lifecycle across form, widget, admin, and backend: - Form: donation_completed, donation_*_failed, donation_slip_conflict, donation_artifact_conflict - Widget: donation_widget_displayed, donation_widget_tts_failed, donation_widget_animation_end, donation_widget_connected, donation_widget_heartbeat_failure - Admin: donation_admin_test_popup, donation_admin_widget_reload, donation_admin_resend - Backend: donation_widget_state_playing, donation_widget_state_shown, donation_resent
This commit is contained in:
@@ -6,10 +6,13 @@ import { donations } from "@/lib/db/schema";
|
||||
import { sse } from "@/lib/db/sse-endpoints";
|
||||
import { fileToDataUrl } from "@/lib/utils";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getPostHogClient } from "@/lib/posthog-server";
|
||||
|
||||
export async function testPopup() {
|
||||
if (!(await adminCheck())) throw new Error("Unauthorized");
|
||||
|
||||
getPostHogClient().capture({ distinctId: "admin", event: "donation_admin_test_popup" });
|
||||
|
||||
sse.donate.pub("ping", {
|
||||
id: "test",
|
||||
name: "Mr. Buzz",
|
||||
@@ -21,6 +24,8 @@ export async function testPopup() {
|
||||
export async function reloadWidget() {
|
||||
if (!(await adminCheck())) throw new Error("Unauthorized");
|
||||
|
||||
getPostHogClient().capture({ distinctId: "admin", event: "donation_admin_widget_reload" });
|
||||
|
||||
sse.donate.pub("refresh", null);
|
||||
}
|
||||
|
||||
@@ -33,6 +38,8 @@ export async function resendPopup(id: string) {
|
||||
.returning();
|
||||
if (!sub) throw new Error("not found");
|
||||
|
||||
getPostHogClient().capture({ distinctId: "admin", event: "donation_admin_resend", properties: { id } });
|
||||
|
||||
sse.donate.pub("ping", {
|
||||
...sub,
|
||||
message: sub.message ?? "",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useEllipsisVisible } from "react-hook-text-overflow";
|
||||
import { ActionButton } from "../../../../components/action-button";
|
||||
import { reloadWidget, resendPopup, testPopup } from "./api";
|
||||
import { useEffect } from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { sse } from "@/lib/db/sse-endpoints";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -65,16 +66,16 @@ const columns: ColumnDef<typeof donations.$inferSelect>[] = [
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<SimpleTooltip text="Send test popup">
|
||||
<ActionButton variant="outline" size="icon-sm" action={testPopup}>
|
||||
<ActionButton variant="outline" size="icon-sm" action={() => { posthog.capture("donation_admin_test_popup"); return testPopup(); }}>
|
||||
<BugPlay />
|
||||
</ActionButton>
|
||||
</SimpleTooltip>
|
||||
<SimpleTooltip text="Reload all widget">
|
||||
<ActionButton
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
action={reloadWidget}
|
||||
>
|
||||
<ActionButton
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
action={() => { posthog.capture("donation_admin_widget_reload"); return reloadWidget(); }}
|
||||
>
|
||||
<ChevronsLeftRightEllipsis />
|
||||
</ActionButton>
|
||||
</SimpleTooltip>
|
||||
@@ -91,11 +92,11 @@ const columns: ColumnDef<typeof donations.$inferSelect>[] = [
|
||||
)}
|
||||
>
|
||||
<SimpleTooltip text="Re-send Popup">
|
||||
<ActionButton
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
action={() => resendPopup(row.row.original.id)}
|
||||
>
|
||||
<ActionButton
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
action={() => { posthog.capture("donation_admin_resend", { id: row.row.original.id }); return resendPopup(row.row.original.id); }}
|
||||
>
|
||||
<MessageCircleWarning />
|
||||
</ActionButton>
|
||||
</SimpleTooltip>
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
SlipUpload,
|
||||
} from "../rubgram/admin/@modal/manual/client";
|
||||
import { DownloadButton } from "../rubgram/client";
|
||||
import { getPostHogClient } from "@/lib/posthog-server";
|
||||
|
||||
const { TMN_DEST_PHONE_NUM, SASTIFY_API_PRIVKEY } = process.env as Record<
|
||||
string,
|
||||
@@ -103,18 +104,23 @@ export default async function () {
|
||||
const { $, error } = formParse(Schema, data);
|
||||
if (error) return { error };
|
||||
|
||||
const ph = getPostHogClient();
|
||||
const distinctId = crypto.randomUUID();
|
||||
|
||||
return await db.transaction(async (tx): Promise<FormSubmitResult> => {
|
||||
if ($.type === "pp") {
|
||||
const arrayBuffer = await $.slip.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
const processed = await checkSlip(buffer, $.slip.type, $.amount);
|
||||
if (!processed.success)
|
||||
if (!processed.success) {
|
||||
ph.capture({ distinctId, event: "donation_slip_check_failed", properties: { amount: $.amount, code: processed.code, message: processed.message } });
|
||||
return {
|
||||
error: {
|
||||
where: "slip",
|
||||
what: `${processed.code}: ${processed.message}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const [check] = await tx
|
||||
.insert(endgameSlips)
|
||||
.values({
|
||||
@@ -128,8 +134,10 @@ export default async function () {
|
||||
console.log(e);
|
||||
return [{ id: "conflict" }];
|
||||
});
|
||||
if (check.id === "conflict")
|
||||
if (check.id === "conflict") {
|
||||
ph.capture({ distinctId, event: "donation_slip_conflict", properties: { amount: $.amount } });
|
||||
return { error: { where: "slip", what: "สลิปนี้ถูกใช้ไปแล้ว" } };
|
||||
}
|
||||
} else {
|
||||
const res: SastifyApiResponse = await fetch(
|
||||
"https://api.sastify.xyz/v1/gateway/tmn",
|
||||
@@ -149,8 +157,10 @@ export default async function () {
|
||||
)
|
||||
.then((r) => r.json())
|
||||
.catch((e) => e);
|
||||
if (!res.success)
|
||||
if (!res.success) {
|
||||
ph.capture({ distinctId, event: "donation_payment_failed", properties: { amount: $.amount, message: res.message } });
|
||||
return { error: { where: "link", what: res.message } };
|
||||
}
|
||||
}
|
||||
|
||||
const { name, amount, message, image } = $;
|
||||
@@ -186,6 +196,7 @@ export default async function () {
|
||||
.catch(() => "conflict");
|
||||
if (res === "conflict") {
|
||||
tx.rollback();
|
||||
ph.capture({ distinctId, event: "donation_artifact_conflict", properties: { amount: $.amount, uid: $.uid } });
|
||||
return { error: { where: "uid", what: "ไม่สามารถสร้างคิวลัดได้" } };
|
||||
}
|
||||
}
|
||||
@@ -199,6 +210,18 @@ export default async function () {
|
||||
});
|
||||
else sse.donate.pub("update", null);
|
||||
|
||||
ph.capture({
|
||||
distinctId,
|
||||
event: "donation_completed",
|
||||
properties: {
|
||||
amount: $.amount,
|
||||
payment_method: $.type,
|
||||
artifact: $.artifact === "true",
|
||||
has_image: !!image,
|
||||
on_screen: $.amount >= 10,
|
||||
},
|
||||
});
|
||||
|
||||
return { toast: "ส่งเรียบร้อย", reset: true };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { sse } from "@/lib/db/sse-endpoints";
|
||||
import { fileToDataUrl } from "@/lib/utils";
|
||||
import { and, asc, not, sql } from "drizzle-orm";
|
||||
import { NextRequest } from "next/server";
|
||||
import { getPostHogClient } from "@/lib/posthog-server";
|
||||
|
||||
export async function PATCH(req: NextRequest) {
|
||||
const tag = Number(req.nextUrl.searchParams.get("tag") ?? "abc");
|
||||
@@ -23,6 +24,7 @@ export async function PATCH(req: NextRequest) {
|
||||
.limit(1)
|
||||
.orderBy(asc(donations.id));
|
||||
if (!havent) return;
|
||||
getPostHogClient().capture({ distinctId: String(havent.id), event: "donation_resent", properties: { donation_id: havent.id, amount: havent.amount } });
|
||||
sse.donate.pub("ping", {
|
||||
...havent,
|
||||
message: havent.message ?? "",
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
import { db } from "@/lib/db";
|
||||
import { donations } from "@/lib/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getPostHogClient } from "@/lib/posthog-server";
|
||||
|
||||
export async function markRunning(id: string) {
|
||||
getPostHogClient().capture({ distinctId: id, event: "donation_widget_state_playing", properties: { donation_id: id } });
|
||||
|
||||
await db
|
||||
.update(donations)
|
||||
.set({ lastPing: new Date() })
|
||||
@@ -12,5 +15,7 @@ export async function markRunning(id: string) {
|
||||
}
|
||||
|
||||
export async function markDone(id: string) {
|
||||
getPostHogClient().capture({ distinctId: id, event: "donation_widget_state_shown", properties: { donation_id: id } });
|
||||
|
||||
await db.update(donations).set({ sent: true }).where(eq(donations.id, id));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import posthog from "posthog-js";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { VersionCheck } from "@/app/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -47,8 +48,10 @@ export default function () {
|
||||
(tts.onerror = () => r(false))
|
||||
),
|
||||
);
|
||||
if (!ttsAvailable) posthog.capture("donation_widget_tts_failed", { amount: data.amount });
|
||||
console.log("Transitioning in");
|
||||
setMounted(true);
|
||||
posthog.capture("donation_widget_displayed", { amount: data.amount, name_length: data.name.length, has_image: !!data.image });
|
||||
console.log("Running SFX");
|
||||
if (sfx.current) {
|
||||
const player = sfx.current;
|
||||
@@ -61,6 +64,7 @@ export default function () {
|
||||
if (ttsAvailable) await new Promise((r) => (tts.onended = r));
|
||||
console.log("Done");
|
||||
markDone(data.id);
|
||||
posthog.capture("donation_widget_animation_end", { amount: data.amount, tts_available: ttsAvailable });
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
setMounted(false);
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
@@ -77,6 +81,7 @@ export default function () {
|
||||
ping: enqueue,
|
||||
refresh: () => location.reload(),
|
||||
});
|
||||
posthog.capture("donation_widget_connected");
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
const tag = Math.floor(Math.random() * 1000);
|
||||
@@ -93,6 +98,7 @@ export default function () {
|
||||
} catch {
|
||||
console.error("Heartbeat timed out");
|
||||
setFailed((x) => x + 1);
|
||||
posthog.capture("donation_widget_heartbeat_failure", { fail_count: failed + 1 });
|
||||
if (failed > 12) location.reload();
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
Reference in New Issue
Block a user