From 695dbf386944864c1e5ec2a4e130ac3340402f77 Mon Sep 17 00:00:00 2001 From: nfmelendez Date: Wed, 23 Oct 2024 09:39:19 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=88=20server:=20setup=20segment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit co-authored-by: danilo neves cruz --- server/api/auth/registration.ts | 2 ++ server/api/card.ts | 5 ++++- server/hooks/cryptomate.ts | 6 ++++++ server/index.ts | 7 ++++++- server/utils/cryptomate.ts | 8 ++++++-- server/utils/segment.ts | 34 +++++++++++++++++++++++++++++++++ server/vitest.config.mts | 1 + 7 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 server/utils/segment.ts diff --git a/server/api/auth/registration.ts b/server/api/auth/registration.ts index c7ce52af..dd48b05c 100644 --- a/server/api/auth/registration.ts +++ b/server/api/auth/registration.ts @@ -20,6 +20,7 @@ import authSecret from "../../utils/authSecret"; import decodePublicKey from "../../utils/decodePublicKey"; import deriveAddress from "../../utils/deriveAddress"; import redis from "../../utils/redis"; +import { identify } from "../../utils/segment"; if (!process.env.ALCHEMY_ACTIVITY_ID) throw new Error("missing alchemy activity id"); const webhookId = process.env.ALCHEMY_ACTIVITY_ID; @@ -143,6 +144,7 @@ export default app }) .catch((error: unknown) => captureException(error)), ]); + identify({ userId: account }); return c.json( { credentialId: credential.id, factory: exaAccountFactoryAddress, x, y, auth: expires.getTime() }, diff --git a/server/api/card.ts b/server/api/card.ts index 7d807cee..a31aad29 100644 --- a/server/api/card.ts +++ b/server/api/card.ts @@ -12,6 +12,7 @@ import database, { cards, credentials } from "../database"; import auth from "../middleware/auth"; import { createCard, getPAN } from "../utils/cryptomate"; import { getInquiry } from "../utils/persona"; +import { track } from "../utils/segment"; const mutexes = new Map(); function createMutex(credentialId: string) { @@ -118,7 +119,8 @@ export default app }, }); if (!credential) return c.json("credential not found", 401); - setUser({ id: parse(Address, credential.account) }); + const account = parse(Address, credential.account); + setUser({ id: account }); if (credential.cards.length === 0 || !credential.cards[0]) return c.json("no card found", 404); const card = credential.cards[0]; switch (patch.type) { @@ -132,6 +134,7 @@ export default app const { status } = patch; if (card.status === status) return c.json(`card is already ${status.toLowerCase()}`, 400); await database.update(cards).set({ status }).where(eq(cards.id, card.id)); + track({ userId: account, event: status === "FROZEN" ? "CardFrozen" : "CardUnfrozen" }); return c.json({ status }, 200); } } diff --git a/server/hooks/cryptomate.ts b/server/hooks/cryptomate.ts index 36f8ca30..0bcd0dc1 100644 --- a/server/hooks/cryptomate.ts +++ b/server/hooks/cryptomate.ts @@ -42,6 +42,7 @@ import { auditorAbi, issuerCheckerAbi, issuerCheckerAddress, marketAbi } from ". import COLLECTOR from "../utils/COLLECTOR"; import keeper from "../utils/keeper"; import publicClient from "../utils/publicClient"; +import { track } from "../utils/segment"; import traceClient, { type CallFrame } from "../utils/traceClient"; import transactionOptions from "../utils/transactionOptions"; @@ -202,6 +203,11 @@ export default new Hono().post( captureException(new Error("bad collection"), { level: "warning", contexts: { tx: { call, trace } } }); return c.json({ response_code: "51" }); } + track({ + userId: account, + event: "TransactionAuthorized", + properties: { usdAmount: payload.data.bill_amount }, + }); return c.json({ response_code: "00" }); } catch (error: unknown) { captureException(error, { contexts: { tx: { call } } }); diff --git a/server/index.ts b/server/index.ts index 73572136..4bdc8417 100644 --- a/server/index.ts +++ b/server/index.ts @@ -2,7 +2,7 @@ import type { Base64URL } from "@exactly/common/validation"; import { serve } from "@hono/node-server"; import { serveStatic } from "@hono/node-server/serve-static"; import { sentry } from "@hono/sentry"; -import { captureException } from "@sentry/node"; +import { captureException, withScope } from "@sentry/node"; import { Hono } from "hono"; import { cors } from "hono/cors"; import { trimTrailingSlash } from "hono/trailing-slash"; @@ -18,6 +18,7 @@ import block from "./hooks/block"; import cryptomate from "./hooks/cryptomate"; import androidFingerprint from "./utils/android/fingerprint"; import appOrigin from "./utils/appOrigin"; +import { closeAndFlush } from "./utils/segment"; const app = new Hono(); app.use(sentry()); @@ -58,6 +59,10 @@ app.onError((error, c) => { serve(app); +["SIGINT", "SIGTERM"].map((code) => + process.on(code, () => closeAndFlush().catch((error: unknown) => captureException(error, { level: "error" }))), +); + declare module "hono" { interface ContextVariableMap { credentialId: Base64URL; diff --git a/server/utils/cryptomate.ts b/server/utils/cryptomate.ts index 931b5805..2df51249 100644 --- a/server/utils/cryptomate.ts +++ b/server/utils/cryptomate.ts @@ -2,11 +2,13 @@ import { Address } from "@exactly/common/validation"; import removeAccents from "remove-accents"; import * as v from "valibot"; +import { track } from "./segment"; + if (!process.env.CRYPTOMATE_URL || !process.env.CRYPTOMATE_API_KEY) throw new Error("missing cryptomate vars"); const baseURL = process.env.CRYPTOMATE_URL; const apiKey = process.env.CRYPTOMATE_API_KEY; -export function createCard({ +export async function createCard({ account, email, name, @@ -21,7 +23,7 @@ export function createCard({ }) { let cardholder = [name.first, name.middle, name.last].filter(Boolean).join(" "); if (cardholder.length > 26 && name.middle) cardholder = `${name.first} ${name.last}`; - return request( + const card = await request( CreateCardResponse, "/cards/virtual-cards/create", v.parse(CreateCardRequest, { @@ -36,6 +38,8 @@ export function createCard({ metadata: { account }, }), ); + track({ event: "CardIssued", userId: account }); + return card; } export async function getPAN(cardId: string) { diff --git a/server/utils/segment.ts b/server/utils/segment.ts new file mode 100644 index 00000000..d11ea369 --- /dev/null +++ b/server/utils/segment.ts @@ -0,0 +1,34 @@ +import type { Address } from "@exactly/common/validation"; +import { Analytics } from "@segment/analytics-node"; +import { captureException } from "@sentry/node"; + +if (!process.env.SEGMENT_WRITE_KEY) throw new Error("missing segment write key"); + +const analytics = new Analytics({ writeKey: process.env.SEGMENT_WRITE_KEY }); + +export function identify( + user: Prettify<{ userId: Address } & Omit[0], "userId">>, +) { + analytics.identify(user); +} + +export function track( + action: Id< + | { event: "CardIssued" } + | { event: "CardFrozen" } + | { event: "CardUnfrozen" } + | { event: "TransactionAuthorized"; properties: { usdAmount: number } } + >, +) { + analytics.track(action); +} + +export function closeAndFlush() { + return analytics.closeAndFlush(); +} + +analytics.on("error", (error) => captureException(error, { level: "error" })); + +type Id = Prettify<{ userId: Address } & T>; + +type Prettify = { [K in keyof T]: T[K] } & unknown; diff --git a/server/vitest.config.mts b/server/vitest.config.mts index 4521adb5..ed864063 100644 --- a/server/vitest.config.mts +++ b/server/vitest.config.mts @@ -17,6 +17,7 @@ export default defineConfig({ KEEPER_PRIVATE_KEY: padHex("0x69"), POSTGRES_URL: "postgres", REDIS_URL: "redis", + SEGMENT_WRITE_KEY: "segment", }, coverage: { enabled: true, reporter: ["lcov"] }, },