server/laf-cloud/functions/cloudRise/analytics.ts
2026-09-24 17:59:52 +08:00

74 lines
3.8 KiB
TypeScript

import { createHash } from "crypto";
export const TEST_APP_ID = "40e3d5c5f2af49a4a074205564ce5dbb";
export const PRODUCTION_APP_ID = "95993f9ab6f1402a87abe5147827e5e0";
const RECEIVER = "https://data.nika4fun.com/sync_data";
/** Only newly committed transitions generate events; polling never replays historical stages. */
export function collect(before: any, run: any, isDebug: any, now: number): any[] {
if (!run) return [];
// Existing Utils.POST uses isDebug=true for RELEASE, false for develop/trial.
const appId = isDebug === true || isDebug === "true" ? PRODUCTION_APP_ID : TEST_APP_ID;
const events: any[] = [];
for (const stage of run.stages) {
const previous = before?.id === run.id && before.stages.find((s: any) =>
s.stage === stage.stage && s.startedAt === stage.startedAt && s.matchId === stage.matchId);
const add = (event: string, properties: any, attempt = "") => {
const id = createHash("sha256").update(JSON.stringify([
run.id, stage.stage, stage.startedAt, stage.matchId, event, attempt,
])).digest("hex");
events.push({ id, event, properties, appId, time: now });
};
if (!previous) add("cloud_rise_start", {
stage: stage.stage, end_count: stage.opponents.filter((o: any) => o.success_num >= stage.target).length,
});
const oldResults = new Set((previous?.results || []).map((r: any) => r.id));
stage.results.forEach((result: any, index: number) => {
if (!oldResults.has(result.id)) add("cloud_rise_step", {
stage: stage.stage, step: index + 1, result: result.outcome === "win" ? "success" : "failure",
}, result.id);
});
if ((stage.status === "lost" || stage.status === "expired") && previous?.status !== stage.status) {
add("cloud_rise_fail", { stage: stage.stage, step: Math.min(stage.target, stage.success_num + 1) });
}
// The exact client-calculated award becomes authoritative only when its receipt is saved.
if (stage.status === "won" && stage.rewardSaved && !previous?.rewardSaved) {
add("cloud_rise_succeed", { stage: stage.stage, coin_amount: stage.reward });
}
}
return events;
}
/** Flush before returning; failed/uncertain deliveries remain in the durable queue for retry. */
export async function send(events: any[], user: any, sdkModule?: any): Promise<string[]> {
if (!events.length) return [];
if (!user.openid && !user.distinctId) return [];
const ThinkingData = sdkModule || require("thinkingdata-node");
const groups = [TEST_APP_ID, PRODUCTION_APP_ID].map(appId => events.filter(e => e.appId === appId));
const delivered = await Promise.all(groups.filter(group => group.length).map(async group => {
const sdk = ThinkingData.initWithBatchMode(group[0].appId, RECEIVER, { batchSize: group.length + 1 });
try {
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("数数上报超时")), 1000);
const finish = (error?: any) => { clearTimeout(timer); error ? reject(error) : resolve(); };
try {
for (const item of group) sdk.trackFirst({
...(user.openid ? { accountId: user.openid } : {}),
...(user.distinctId ? { distinctId: user.distinctId } : {}),
event: item.event, firstCheckId: item.id,
// Align with the existing server SDK's China-local event timestamps.
time: new Date(item.time + 8 * 3600000), properties: item.properties,
callback(error: any) { if (error) finish(error); },
});
sdk.flush(finish);
} catch (error) { finish(error); }
});
return group.map(item => item.id);
} catch (error) {
console.error("cloudRise analytics pending", error instanceof Error ? error.message : error);
return [];
} finally { sdk.close(); }
}));
return delivered.flat();
}