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 { 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((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(); }