add telemetry

This commit is contained in:
Sasha 2024-11-13 13:23:38 +07:00
parent b75eed9aaf
commit 903bd965e1
No known key found for this signature in database
9 changed files with 654 additions and 602 deletions

File diff suppressed because it is too large Load Diff

View File

@ -49,6 +49,7 @@
"@types/react": "^18.3.10", "@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.2", "@vitejs/plugin-react": "^4.3.2",
"@waku/interfaces": "^0.0.29-5674b0e.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"eslint": "^8.56.0", "eslint": "^8.56.0",

View File

@ -161,15 +161,15 @@ const Home: React.FC = () => (
<div className="w-full max-w-sm mx-auto p-4 md:p-6 bg-card rounded-lg shadow-md"> <div className="w-full max-w-sm mx-auto p-4 md:p-6 bg-card rounded-lg shadow-md">
<Link to="create"> <Link to="create">
<Button className="w-full mb-4"> <Button className="w-full mb-4">
Create New Chain Create New Book
</Button> </Button>
</Link> </Link>
<p className="text-sm md:text-base text-muted-foreground"> <p className="text-sm md:text-base text-muted-foreground">
Click the button above to start creating a new chain. Click the button above to start creating a new book.
</p> </p>
</div> </div>
<p className="text-xs md:text-sm text-muted-foreground text-center"> <p className="text-xs md:text-sm text-muted-foreground text-center">
Welcome to BuddyBook - Create and share your chains! Welcome to BuddyBook - Create and share your books!
</p> </p>
</div> </div>
) )

View File

@ -13,6 +13,7 @@ import { useWaku } from '@waku/react';
import { LightNode } from '@waku/sdk'; import { LightNode } from '@waku/sdk';
import { createMessage, encoder } from '@/lib/waku'; import { createMessage, encoder } from '@/lib/waku';
import { useWalletPrompt } from '@/hooks/useWalletPrompt'; import { useWalletPrompt } from '@/hooks/useWalletPrompt';
import { fromLightPush, Telemetry, TelemetryType, buildExtraData } from '@/lib/telemetry';
interface FormData { interface FormData {
title: string; title: string;
@ -48,18 +49,49 @@ const ChainCreationForm: React.FC = () => {
const blockUUID = uuidv4(); const blockUUID = uuidv4();
setCreatedBlockUUID(blockUUID); setCreatedBlockUUID(blockUUID);
const timestamp = Date.now();
const message = createMessage({ const message = createMessage({
chainUUID: formData.uuid, chainUUID: formData.uuid,
blockUUID: blockUUID, blockUUID: blockUUID,
title: formData.title, title: formData.title,
description: formData.description, description: formData.description,
signedMessage: signature, signedMessage: signature,
timestamp: Date.now(), timestamp: timestamp,
signatures: [{address, signature}], signatures: [{address, signature}],
parentBlockUUID: null parentBlockUUID: null
}); });
await node?.lightPush.send(encoder, message) try {
const result = await node?.lightPush.send(encoder, message);
Telemetry.push(fromLightPush({
result,
wallet: address,
bookId: formData.uuid,
node,
encoder,
timestamp,
}));
} catch (e) {
Telemetry.push([{
type: TelemetryType.LIGHT_PUSH_FILTER,
protocol: "lightPush",
timestamp: timestamp,
createdAt: timestamp,
seenTimestamp: timestamp,
peerId: node.peerId.toString(),
contentTopic: encoder.contentTopic,
pubsubTopic: encoder.pubsubTopic,
ephemeral: encoder.ephemeral,
messageHash: uuidv4(),
errorMessage: (e as Error)?.message ?? "Error during LightPush",
extraData: buildExtraData({
wallet: address,
bookId: formData.uuid,
}),
}]);
throw e;
}
setIsSuccess(true); setIsSuccess(true);
setIsSigning(false); setIsSigning(false);
}, },
@ -112,8 +144,8 @@ const ChainCreationForm: React.FC = () => {
const handleSubmit = async () => { const handleSubmit = async () => {
setIsSigning(true); setIsSigning(true);
setSendError(null); setSendError(null);
const message = `Create Chain: const message = `Create Book:
Chain UUID: ${formData.uuid} Book UUID: ${formData.uuid}
Title: ${formData.title} Title: ${formData.title}
Description: ${formData.description} Description: ${formData.description}
Timestamp: ${new Date().getTime()} Timestamp: ${new Date().getTime()}
@ -132,12 +164,12 @@ const ChainCreationForm: React.FC = () => {
return ( return (
<Card className="w-full max-w-2xl mx-auto"> <Card className="w-full max-w-2xl mx-auto">
<CardHeader> <CardHeader>
<CardTitle>Create a New Chain</CardTitle> <CardTitle>Create a New Book</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleCreateChain} className="space-y-6"> <form onSubmit={handleCreateChain} className="space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="title">Chain Title</Label> <Label htmlFor="title">Book Title</Label>
<Input <Input
type="text" type="text"
id="title" id="title"
@ -150,7 +182,7 @@ const ChainCreationForm: React.FC = () => {
{errors.title && <p className="text-sm text-destructive">{errors.title}</p>} {errors.title && <p className="text-sm text-destructive">{errors.title}</p>}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="description">Chain Description</Label> <Label htmlFor="description">Book Description</Label>
<Textarea <Textarea
id="description" id="description"
name="description" name="description"
@ -161,13 +193,13 @@ const ChainCreationForm: React.FC = () => {
/> />
{errors.description && <p className="text-sm text-destructive">{errors.description}</p>} {errors.description && <p className="text-sm text-destructive">{errors.description}</p>}
</div> </div>
<Button type="submit" className="w-full py-6 text-base sm:py-2 sm:text-sm">Create Chain</Button> <Button type="submit" className="w-full py-6 text-base sm:py-2 sm:text-sm">Create Book</Button>
</form> </form>
</CardContent> </CardContent>
<Dialog open={showModal} onOpenChange={handleCloseModal}> <Dialog open={showModal} onOpenChange={handleCloseModal}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>{isSuccess ? "Chain Created" : "Chain Preview"}</DialogTitle> <DialogTitle>{isSuccess ? "Book Created" : "Book Preview"}</DialogTitle>
</DialogHeader> </DialogHeader>
{!isSuccess ? ( {!isSuccess ? (
<> <>

View File

@ -9,6 +9,7 @@ import { Loader2 } from "lucide-react";
import QRCode from '@/components/QRCode'; import QRCode from '@/components/QRCode';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { useWalletPrompt } from '@/hooks/useWalletPrompt'; import { useWalletPrompt } from '@/hooks/useWalletPrompt';
import { fromLightPush, Telemetry } from '@/lib/telemetry';
interface SignChainProps { interface SignChainProps {
block: BlockPayload; block: BlockPayload;
@ -70,19 +71,31 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
return; return;
} }
const timestamp = Date.now();
const newBlock: BlockPayload = { const newBlock: BlockPayload = {
chainUUID: block.chainUUID, chainUUID: block.chainUUID,
blockUUID: uuidv4(), blockUUID: uuidv4(),
title: block.title, title: block.title,
description: block.description, description: block.description,
signedMessage: signature, signedMessage: signature,
timestamp: Date.now(), timestamp,
signatures: [{ address, signature }], signatures: [{ address, signature }],
parentBlockUUID: block.blockUUID parentBlockUUID: block.blockUUID
}; };
const wakuMessage = createMessage(newBlock); const wakuMessage = createMessage(newBlock);
const { failures, successes } = await node.lightPush.send(encoder, wakuMessage); const result = await node.lightPush.send(encoder, wakuMessage);
Telemetry.push(fromLightPush({
result,
node,
encoder,
timestamp,
bookId: block.chainUUID,
wallet: address,
}));
const { failures, successes } = result;
if (failures.length > 0 || successes.length === 0) { if (failures.length > 0 || successes.length === 0) {
throw new Error('Failed to send message to Waku network'); throw new Error('Failed to send message to Waku network');
@ -141,16 +154,16 @@ Signed by: ${ensName || address}`;
return ( return (
<> <>
<Button onClick={() => setIsOpen(true)} disabled={alreadySigned}> <Button onClick={() => setIsOpen(true)} disabled={alreadySigned}>
{alreadySigned ? 'Already Signed' : !address ? 'Connect Wallet' : 'Sign Chain'} {alreadySigned ? 'Already Signed' : !address ? 'Connect Wallet' : 'Sign Book'}
</Button> </Button>
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Sign Chain</DialogTitle> <DialogTitle>Sign Book</DialogTitle>
<DialogDescription> <DialogDescription>
{alreadySigned {alreadySigned
? 'You have already signed this chain.' ? 'You have already signed this book.'
: 'Review the block details and sign to add your signature to the chain.'} : 'Review the block details and sign to add your signature to the book.'}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">

View File

@ -29,7 +29,7 @@ const SignSharedChain: React.FC<SignSharedChainProps> = ({ chainsData, onChainUp
<Card className="w-full max-w-md mx-auto"> <Card className="w-full max-w-md mx-auto">
<CardContent className="flex flex-col items-center justify-center py-8 space-y-4"> <CardContent className="flex flex-col items-center justify-center py-8 space-y-4">
<Loader2 className="h-8 w-8 animate-spin" /> <Loader2 className="h-8 w-8 animate-spin" />
<p className="text-sm text-muted-foreground">Looking for chain...</p> <p className="text-sm text-muted-foreground">Looking for book...</p>
</CardContent> </CardContent>
</Card> </Card>
); );
@ -39,11 +39,11 @@ const SignSharedChain: React.FC<SignSharedChainProps> = ({ chainsData, onChainUp
return ( return (
<Card className="w-full max-w-md mx-auto"> <Card className="w-full max-w-md mx-auto">
<CardHeader> <CardHeader>
<CardTitle>Chain Not Found</CardTitle> <CardTitle>Book Not Found</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="mb-4">The requested chain or block could not be found.</p> <p className="mb-4">The requested book or block could not be found.</p>
<Button onClick={() => navigate('/view')}>View All Chains</Button> <Button onClick={() => navigate('/view')}>View All Books</Button>
</CardContent> </CardContent>
</Card> </Card>
); );
@ -52,7 +52,7 @@ const SignSharedChain: React.FC<SignSharedChainProps> = ({ chainsData, onChainUp
return ( return (
<Card className="w-full max-w-2xl mx-auto"> <Card className="w-full max-w-2xl mx-auto">
<CardHeader> <CardHeader>
<CardTitle>Sign Shared Chain</CardTitle> <CardTitle>Sign Shared Book</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<h2 className="text-xl font-semibold mb-2">{block.title}</h2> <h2 className="text-xl font-semibold mb-2">{block.title}</h2>

View File

@ -66,9 +66,9 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoad
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
<DialogHeader> <DialogHeader>
<DialogTitle>Share this Chain</DialogTitle> <DialogTitle>Share this Book</DialogTitle>
<DialogDescription> <DialogDescription>
Share this chain with others to collect their signatures. Share this book with others to collect their signatures.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
@ -109,18 +109,18 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoad
<Card className="w-full max-w-4xl mx-auto"> <Card className="w-full max-w-4xl mx-auto">
<CardHeader> <CardHeader>
<CardTitle> <CardTitle>
Existing Chains Existing Books
{isLoading && ( {isLoading && (
<span className="ml-2 inline-flex items-center text-muted-foreground text-sm font-normal"> <span className="ml-2 inline-flex items-center text-muted-foreground text-sm font-normal">
<Loader2 className="h-4 w-4 animate-spin mr-2" /> <Loader2 className="h-4 w-4 animate-spin mr-2" />
Loading more chains... Loading more books...
</span> </span>
)} )}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{rootBlocks.length === 0 && !isLoading ? ( {rootBlocks.length === 0 && !isLoading ? (
<p>No chains found.</p> <p>No books found.</p>
) : ( ) : (
<ul className="space-y-4"> <ul className="space-y-4">
{rootBlocks.map((block) => renderBlock(block, 0))} {rootBlocks.map((block) => renderBlock(block, 0))}

View File

@ -0,0 +1,259 @@
import { IDecoder, IEncoder, LightNode, SDKProtocolResult, SubscribeResult } from "@waku/interfaces";
import { v4 as uuidv4 } from 'uuid';
export enum TelemetryType {
LIGHT_PUSH_FILTER = "LightPushFilter",
}
interface TelemetryMessage {
type: string;
timestamp: number;
contentTopic: string;
pubsubTopic: string;
peerId: string;
errorMessage: string;
extraData: string;
}
export interface TelemetryPushFilter extends TelemetryMessage {
type: "LightPushFilter",
protocol: string;
ephemeral: boolean;
seenTimestamp: number;
createdAt: number;
messageHash: string;
}
export class TelemetryClient {
constructor(
private readonly url: string,
private intervalPeriod: number = 5000
) {
this.start();
}
private queue: TelemetryMessage[] = [];
private intervalId: NodeJS.Timeout | null = null;
private requestId = 0;
public push<T extends TelemetryMessage>(messages: T[]) {
this.queue.push(...messages);
}
public async start() {
if (!this.intervalId) {
this.intervalId = setInterval(async () => {
if (this.queue.length > 0) {
const success = await this.send(this.queue);
if (success) {
console.log("Sent ", this.queue.length, " telemetry logs");
this.queue = [];
}
}
}, this.intervalPeriod);
}
}
public stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
private async send<T extends TelemetryMessage>(messages: T[]) {
const isTelemetryOn = localStorage.getItem("telemetryOptIn");
if (!isTelemetryOn || isTelemetryOn === "false" || isTelemetryOn !== "true" || !window.location.hostname.includes("buddybook.fun")) {
return;
}
const telemetryRequests = messages.map((message) => ({
id: ++this.requestId,
telemetryType: message.type.toString(),
telemetryData: message
}));
try {
const res = await fetch(this.url, {
method: "POST",
body: JSON.stringify(telemetryRequests),
});
if (res.status !== 201) {
console.log("DEBUG: Error sending messages to telemetry service: ", res.status, res.statusText, res.json);
return false
}
return true;
} catch (e) {
console.log("DEBUG: Error sending messages to telemetry service", e);
return false;
}
}
}
export const Telemetry = new TelemetryClient("https://telemetry.status.im/waku-metrics", 5000);
type ExtraData = {
wallet?: string;
bookId?: string;
timeTaken?: number;
};
export const buildExtraData = ({
wallet,
bookId,
timeTaken,
}: ExtraData): string => {
return JSON.stringify({
sdk: "@waku/react:0.0.7-9a7287d",
wallet,
bookId,
timeTaken,
});
};
type FromLightPush = {
node: LightNode,
timestamp: number,
encoder: IEncoder,
wallet: string,
bookId: string,
result: SDKProtocolResult,
}
export const fromLightPush = (data: FromLightPush): TelemetryPushFilter[] => {
const telemetry: TelemetryPushFilter[] = [];
data.result?.successes?.forEach((success) => {
telemetry.push({
type: TelemetryType.LIGHT_PUSH_FILTER,
protocol: "lightPush",
timestamp: data.timestamp,
createdAt: data.timestamp,
seenTimestamp: data.timestamp,
peerId: success.toString(),
contentTopic: data.encoder.contentTopic,
pubsubTopic: data.encoder.pubsubTopic,
ephemeral: false,
messageHash: uuidv4(),
errorMessage: "",
extraData: buildExtraData({
bookId: data.bookId,
wallet: data.wallet,
}),
});
});
data.result?.failures?.forEach((fail) => {
telemetry.push({
type: TelemetryType.LIGHT_PUSH_FILTER,
protocol: "lightPush",
timestamp: data.timestamp,
createdAt: data.timestamp,
seenTimestamp: data.timestamp,
peerId: fail?.peerId?.toString() || "missing",
contentTopic: data.encoder.contentTopic,
pubsubTopic: data.encoder.pubsubTopic,
ephemeral: data.encoder.ephemeral,
messageHash: uuidv4(),
errorMessage: fail.error.toString(),
extraData: buildExtraData({
wallet: data.wallet,
bookId: data.bookId,
}),
});
});
return telemetry;
};
type FromFilter = {
result: SubscribeResult,
node: LightNode,
timestamp: number,
decoder: IDecoder<any>,
};
export const fromFilter = (data: FromFilter): TelemetryPushFilter[] => {
const telemetry: TelemetryPushFilter[] = [];
const { error, results } = data.result;
if (error) {
telemetry.push({
type: TelemetryType.LIGHT_PUSH_FILTER,
protocol: "filter",
timestamp: data.timestamp,
createdAt: data.timestamp,
seenTimestamp: data.timestamp,
peerId: data.node.peerId.toString(),
contentTopic: data.decoder.contentTopic,
pubsubTopic: data.decoder.pubsubTopic,
ephemeral: false,
messageHash: uuidv4(),
errorMessage: error,
extraData: buildExtraData({}),
});
}
results?.failures?.forEach((fail) => {
telemetry.push({
type: TelemetryType.LIGHT_PUSH_FILTER,
protocol: "filter",
timestamp: data.timestamp,
createdAt: data.timestamp,
seenTimestamp: data.timestamp,
peerId: fail?.peerId?.toString() || "",
contentTopic: data.decoder.contentTopic,
pubsubTopic: data.decoder.pubsubTopic,
ephemeral: false,
messageHash: uuidv4(),
errorMessage: fail?.error || "Unknown error",
extraData: buildExtraData({}),
});
});
results?.successes?.forEach((success) => {
telemetry.push({
type: TelemetryType.LIGHT_PUSH_FILTER,
protocol: "filter",
timestamp: data.timestamp,
createdAt: data.timestamp,
seenTimestamp: data.timestamp,
peerId: success.toString(),
contentTopic: data.decoder.contentTopic,
pubsubTopic: data.decoder.pubsubTopic,
ephemeral: false,
messageHash: uuidv4(),
errorMessage: "",
extraData: buildExtraData({}),
});
});
return telemetry;
};
type FromStore = {
timestamp: number,
timeTaken: number,
node: LightNode,
decoder: IDecoder<any>,
};
export const fromStore = (data: FromStore): TelemetryPushFilter[] => {
return [{
type: TelemetryType.LIGHT_PUSH_FILTER,
protocol: "filter",
timestamp: data.timestamp,
createdAt: data.timestamp,
seenTimestamp: data.timestamp,
peerId: data.node.peerId.toString(),
contentTopic: data.decoder.contentTopic,
pubsubTopic: data.decoder.pubsubTopic,
ephemeral: false,
messageHash: uuidv4(),
errorMessage: "",
extraData: buildExtraData({
timeTaken
}),
}];
};

View File

@ -1,11 +1,11 @@
import { createEncoder, createDecoder, type LightNode, type CreateWakuNodeOptions } from "@waku/sdk"; import { createEncoder, createDecoder, type LightNode, type CreateWakuNodeOptions } from "@waku/sdk";
import protobuf from 'protobufjs'; import protobuf from 'protobufjs';
import { Telemetry, fromFilter, fromStore } from "./telemetry";
export const WAKU_NODE_OPTIONS: CreateWakuNodeOptions = { defaultBootstrap: true, nodeToUse: { export const WAKU_NODE_OPTIONS: CreateWakuNodeOptions = { defaultBootstrap: true, nodeToUse: {
store: "/dns4/store-02.ac-cn-hongkong-c.status.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmU7xtcwytXpGpeDrfyhJkiFvTkQbLB9upL5MXPLGceG9K" store: "/dns4/store-02.ac-cn-hongkong-c.status.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmU7xtcwytXpGpeDrfyhJkiFvTkQbLB9upL5MXPLGceG9K"
} }; } };
export type Signature = { export type Signature = {
address: `0x${string}`; address: `0x${string}`;
signature: string; signature: string;
@ -22,7 +22,7 @@ export type BlockPayload = {
parentBlockUUID: string | null; parentBlockUUID: string | null;
} }
const contentTopic = "/buddybook-dogfood/1/chain/proto"; export const contentTopic = "/buddybook-dogfood/1/chain/proto";
export const encoder = createEncoder({ export const encoder = createEncoder({
contentTopic: contentTopic, contentTopic: contentTopic,
@ -66,7 +66,8 @@ export function createMessage({
} }
export async function* getMessagesFromStore(node: LightNode) { export async function* getMessagesFromStore(node: LightNode) {
console.time("getMessagesFromStore") try {
const startTime = performance.now();
for await (const messagePromises of node.store.queryGenerator([decoder])) { for await (const messagePromises of node.store.queryGenerator([decoder])) {
const messages = await Promise.all(messagePromises); const messages = await Promise.all(messagePromises);
for (const message of messages) { for (const message of messages) {
@ -77,11 +78,23 @@ export async function* getMessagesFromStore(node: LightNode) {
yield blockPayload; yield blockPayload;
} }
} }
console.timeEnd("getMessagesFromStore") const endTime = performance.now();
const timeTaken = endTime - startTime;
console.log("getMessagesFromStore", timeTaken)
Telemetry.push(fromStore({
node,
decoder,
timestamp: startTime,
timeTaken,
}));
} catch(e) {
throw e;
}
} }
export async function subscribeToFilter(node: LightNode, callback: (message: BlockPayload) => void) { export async function subscribeToFilter(node: LightNode, callback: (message: BlockPayload) => void) {
const {error, subscription, results} = await node.filter.subscribe([decoder], (message) => { const result = await node.filter.subscribe([decoder], (message) => {
console.log('message received from filter', message) console.log('message received from filter', message)
if (message.payload) { if (message.payload) {
const blockPayload = block.decode(message.payload) as unknown as BlockPayload; const blockPayload = block.decode(message.payload) as unknown as BlockPayload;
@ -90,6 +103,14 @@ export async function subscribeToFilter(node: LightNode, callback: (message: Blo
} }
}, {forceUseAllPeers: false}); }, {forceUseAllPeers: false});
Telemetry.push(fromFilter({
result,
node,
decoder,
timestamp: Date.now(),
}));
const {error, subscription, results} = result;
console.log("results", results) console.log("results", results)
if (error) { if (error) {