Skip to content

Commit 8afedd7

Browse files
authored
Client Analytics (#394)
* Added basic skeleton * Added Tx Analytics Reporting * Added functionName & Extension from Db to analytics for worker only * updated retryTx to send usage stats * updated to add correct value for retryCount * updated implementation to send more data * Added msSinceQueue, msSinceSend metrics for reporting * updated reportUsage error logging * Added request response time to analytics reporting * updated Env var for analytics to allow '' * made reportUsage() synchronous
1 parent 0ef3f38 commit 8afedd7

File tree

11 files changed

+519
-88
lines changed

11 files changed

+519
-88
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
"@t3-oss/env-core": "^0.6.0",
4444
"@thirdweb-dev/auth": "^4.1.27",
4545
"@thirdweb-dev/chains": "0.1.70",
46-
"@thirdweb-dev/sdk": "0.0.0-dev-d3311a1-20240210064223",
47-
"@thirdweb-dev/service-utils": "^0.4.2",
46+
"@thirdweb-dev/sdk": "4.0.36-nightly-fa637c2e3-20240214074441",
47+
"@thirdweb-dev/service-utils": "0.4.17",
4848
"@thirdweb-dev/wallets": "^2.1.5",
4949
"body-parser": "^1.20.2",
5050
"cookie": "^0.5.0",

src/server/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { deleteAllWalletNonces } from "../db/wallets/deleteAllWalletNonces";
77
import { clearCacheCron } from "../utils/cron/clearCacheCron";
88
import { env } from "../utils/env";
99
import { logger } from "../utils/logger";
10+
import { withServerUsageReporting } from "../utils/usage";
1011
import { updateTxListener } from "./listerners/updateTxListener";
1112
import { withAuth } from "./middleware/auth";
1213
import { withCors } from "./middleware/cors";
@@ -66,6 +67,7 @@ export const initServer = async () => {
6667
await withExpress(server);
6768
await withOpenApi(server);
6869
await withRoutes(server);
70+
await withServerUsageReporting(server);
6971

7072
await server.ready();
7173

src/server/middleware/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import { getConfig } from "../../utils/cache/getConfig";
1818
import { getWebhook } from "../../utils/cache/getWebhook";
1919
import { env } from "../../utils/env";
2020
import { logger } from "../../utils/logger";
21+
import { sendWebhookRequest } from "../../utils/webhook";
2122
import { Permission } from "../schemas/auth";
22-
import { sendWebhookRequest } from "../utils/webhook";
2323

2424
export type TAuthData = never;
2525
export type TAuthSession = { permissions: string };

src/utils/env.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ export const env = createEnv({
7171
ENABLE_HTTPS: boolSchema("false"),
7272
HTTPS_PASSPHRASE: z.string().default("thirdweb-engine"),
7373
PRUNE_TRANSACTIONS: boolSchema("true"),
74+
CLIENT_ANALYTICS_URL: z
75+
.union([UrlSchema, z.literal("")])
76+
.default("https://c.thirdweb.com/event"),
7477
SDK_BATCH_TIME_LIMIT: z.coerce.number().default(0),
7578
SDK_BATCH_SIZE_LIMIT: z.coerce.number().default(100),
7679
},
@@ -90,6 +93,7 @@ export const env = createEnv({
9093
ENABLE_HTTPS: process.env.ENABLE_HTTPS,
9194
HTTPS_PASSPHRASE: process.env.HTTPS_PASSPHRASE,
9295
PRUNE_TRANSACTIONS: process.env.PRUNE_TRANSACTIONS,
96+
CLIENT_ANALYTICS_URL: process.env.CLIENT_ANALYTICS_URL,
9397
SDK_BATCH_TIME_LIMIT: process.env.SDK_BATCH_TIME_LIMIT,
9498
SDK_BATCH_SIZE_LIMIT: process.env.SDK_BATCH_SIZE_LIMIT,
9599
},

src/utils/usage.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { Static, Type } from "@sinclair/typebox";
2+
import { TransactionErrorInfo } from "@thirdweb-dev/sdk";
3+
import { UsageEvent } from "@thirdweb-dev/service-utils/cf-worker";
4+
import { FastifyInstance } from "fastify";
5+
import { contractParamSchema } from "../server/schemas/sharedApiSchemas";
6+
import { walletParamSchema } from "../server/schemas/wallet";
7+
import { getChainIdFromChain } from "../server/utils/chain";
8+
import { deriveClientId } from "./api-keys";
9+
import { env } from "./env";
10+
import { logger } from "./logger";
11+
12+
type CreateHeaderForRequestParams = {
13+
clientId: string;
14+
backendwalletAddress?: string;
15+
chainId?: string;
16+
};
17+
18+
export interface ReportUsageParams {
19+
action: UsageEventTxActionEnum;
20+
input: {
21+
chainId?: string;
22+
fromAddress?: string;
23+
toAddress?: string;
24+
value?: string;
25+
transactionHash?: string;
26+
onChainTxStatus?: number;
27+
userOpHash?: string;
28+
functionName?: string;
29+
extension?: string;
30+
retryCount?: number;
31+
provider?: string;
32+
transactionValue?: string;
33+
msSinceQueue?: number;
34+
msSinceSend?: number;
35+
};
36+
error?: TransactionErrorInfo;
37+
}
38+
39+
const EngineRequestParams = Type.Object({
40+
...contractParamSchema.properties,
41+
...walletParamSchema.properties,
42+
});
43+
44+
export enum UsageEventTxActionEnum {
45+
MineTx = "mine_tx",
46+
NotSendTx = "not_send_tx",
47+
QueueTx = "queue_tx",
48+
SendTx = "send_tx",
49+
CancelTx = "cancel_tx",
50+
APIRequest = "api_request",
51+
}
52+
53+
interface UsageEventSchema extends Omit<UsageEvent, "action"> {
54+
action: UsageEventTxActionEnum;
55+
}
56+
57+
const createHeaderForRequest = (input: CreateHeaderForRequestParams) => {
58+
return {
59+
"Content-Type": "application/json",
60+
"x-sdk-version": process.env.ENGINE_VERSION,
61+
"x-product-name": "engine",
62+
"x-client-id": input.clientId,
63+
} as HeadersInit;
64+
};
65+
66+
export const withServerUsageReporting = (server: FastifyInstance) => {
67+
server.addHook("onResponse", async (request, reply) => {
68+
try {
69+
// If the CLIENT_ANALYTICS_URL is not set, then we don't want to report usage
70+
if (env.CLIENT_ANALYTICS_URL === "") {
71+
return;
72+
}
73+
74+
const derivedClientId = deriveClientId(env.THIRDWEB_API_SECRET_KEY);
75+
const headers = createHeaderForRequest({
76+
clientId: derivedClientId,
77+
});
78+
79+
const requestParams = request?.params as Static<
80+
typeof EngineRequestParams
81+
>;
82+
83+
const chainId = requestParams?.chain
84+
? await getChainIdFromChain(requestParams.chain)
85+
: "";
86+
87+
if (reply.request.routerPath === "" || !reply.request.routerPath) {
88+
return;
89+
}
90+
91+
const requestBody: UsageEventSchema = {
92+
source: "engine",
93+
action: UsageEventTxActionEnum.APIRequest,
94+
clientId: derivedClientId,
95+
pathname: reply.request.routerPath,
96+
chainId: chainId || undefined,
97+
walletAddress: requestParams.walletAddress || undefined,
98+
contractAddress: requestParams.contractAddress || undefined,
99+
httpStatusCode: reply.statusCode,
100+
msTotalDuration: Math.ceil(reply.getResponseTime()),
101+
};
102+
103+
fetch(env.CLIENT_ANALYTICS_URL, {
104+
method: "POST",
105+
headers,
106+
body: JSON.stringify(requestBody),
107+
});
108+
} catch (e) {}
109+
});
110+
};
111+
112+
export const reportUsage = (usageParams: ReportUsageParams[]) => {
113+
try {
114+
usageParams.map(async (item) => {
115+
try {
116+
// If the CLIENT_ANALYTICS_URL is not set, then we don't want to report usage
117+
if (env.CLIENT_ANALYTICS_URL === "") {
118+
return;
119+
}
120+
121+
const derivedClientId = deriveClientId(env.THIRDWEB_API_SECRET_KEY);
122+
const headers = createHeaderForRequest({
123+
clientId: derivedClientId,
124+
});
125+
126+
const chainId = item.input.chainId
127+
? parseInt(item.input.chainId)
128+
: undefined;
129+
const requestBody: UsageEventSchema = {
130+
source: "engine",
131+
action: item.action,
132+
clientId: derivedClientId,
133+
chainId,
134+
walletAddress: item.input.fromAddress || undefined,
135+
contractAddress: item.input.toAddress || undefined,
136+
transactionValue: item.input.value || undefined,
137+
transactionHash: item.input.transactionHash || undefined,
138+
userOpHash: item.input.userOpHash || undefined,
139+
errorCode: item.input.onChainTxStatus
140+
? item.input.onChainTxStatus === 0
141+
? "EXECUTION_REVERTED"
142+
: undefined
143+
: item.error?.reason || undefined,
144+
functionName: item.input.functionName || undefined,
145+
extension: item.input.extension || undefined,
146+
retryCount: item.input.retryCount || undefined,
147+
provider: item.input.provider || undefined,
148+
msSinceSend: item.input.msSinceSend || undefined,
149+
msSinceQueue: item.input.msSinceQueue || undefined,
150+
};
151+
152+
fetch(env.CLIENT_ANALYTICS_URL, {
153+
method: "POST",
154+
headers,
155+
body: JSON.stringify(requestBody),
156+
});
157+
} catch (e) {}
158+
});
159+
} catch (error) {
160+
logger({
161+
service: "worker",
162+
level: "error",
163+
message: `Error:`,
164+
error,
165+
});
166+
}
167+
};

src/server/utils/webhook.ts renamed to src/utils/webhook.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import crypto from "crypto";
2-
import { getTxByIds } from "../../db/transactions/getTxByIds";
2+
import { getTxByIds } from "../db/transactions/getTxByIds";
33
import {
44
SanitizedWebHooksSchema,
55
WalletBalanceWebhookSchema,
66
WebhooksEventTypes,
7-
} from "../../schema/webhooks";
8-
import { getWebhook } from "../../utils/cache/getWebhook";
9-
import { logger } from "../../utils/logger";
10-
import { TransactionStatusEnum } from "../schemas/transaction";
7+
} from "../schema/webhooks";
8+
import { TransactionStatusEnum } from "../server/schemas/transaction";
9+
import { getWebhook } from "./cache/getWebhook";
10+
import { logger } from "./logger";
1111

1212
let balanceNotificationLastSentAt = -1;
1313

0 commit comments

Comments
 (0)