chore: add telemetry to buddy book and change text to use Book instea… (#110)

This commit is contained in:
Sasha 2024-11-14 14:00:40 +07:00 committed by GitHub
parent 87d1499aa1
commit 7613b1e9e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 684 additions and 610 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -206,15 +206,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">
<Link to="create">
<Button className="w-full mb-4">
Create New Chain
Create New Book
</Button>
</Link>
<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>
</div>
<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>
</div>
)

View File

@ -13,6 +13,7 @@ import { useWaku } from '@waku/react';
import { LightNode } from '@waku/sdk';
import { createMessage, encoder } from '@/lib/waku';
import { useWalletPrompt } from '@/hooks/useWalletPrompt';
import { fromLightPush, Telemetry, TelemetryType, buildExtraData, toInt } from '@/lib/telemetry';
interface FormData {
title: string;
@ -48,18 +49,49 @@ const ChainCreationForm: React.FC = () => {
const blockUUID = uuidv4();
setCreatedBlockUUID(blockUUID);
const timestamp = Date.now();
const message = createMessage({
chainUUID: formData.uuid,
blockUUID: blockUUID,
title: formData.title,
description: formData.description,
signedMessage: signature,
timestamp: Date.now(),
timestamp: timestamp,
signatures: [{address, signature}],
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: toInt(timestamp),
createdAt: toInt(timestamp),
seenTimestamp: toInt(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);
setIsSigning(false);
},
@ -112,8 +144,8 @@ const ChainCreationForm: React.FC = () => {
const handleSubmit = async () => {
setIsSigning(true);
setSendError(null);
const message = `Create Chain:
Chain UUID: ${formData.uuid}
const message = `Create Book:
Book UUID: ${formData.uuid}
Title: ${formData.title}
Description: ${formData.description}
Timestamp: ${new Date().getTime()}
@ -132,12 +164,12 @@ const ChainCreationForm: React.FC = () => {
return (
<Card className="w-full max-w-2xl mx-auto">
<CardHeader>
<CardTitle>Create a New Chain</CardTitle>
<CardTitle>Create a New Book</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleCreateChain} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="title">Chain Title</Label>
<Label htmlFor="title">Book Title</Label>
<Input
type="text"
id="title"
@ -150,7 +182,7 @@ const ChainCreationForm: React.FC = () => {
{errors.title && <p className="text-sm text-destructive">{errors.title}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="description">Chain Description</Label>
<Label htmlFor="description">Book Description</Label>
<Textarea
id="description"
name="description"
@ -161,13 +193,13 @@ const ChainCreationForm: React.FC = () => {
/>
{errors.description && <p className="text-sm text-destructive">{errors.description}</p>}
</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>
</CardContent>
<Dialog open={showModal} onOpenChange={handleCloseModal}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{isSuccess ? "Chain Created" : "Chain Preview"}</DialogTitle>
<DialogTitle>{isSuccess ? "Book Created" : "Book Preview"}</DialogTitle>
</DialogHeader>
{!isSuccess ? (
<>

View File

@ -9,6 +9,7 @@ import { Loader2 } from "lucide-react";
import QRCode from '@/components/QRCode';
import { useWalletPrompt } from '@/hooks/useWalletPrompt';
import { v4 as uuidv4 } from 'uuid';
import { fromLightPush, Telemetry } from '@/lib/telemetry';
interface SignChainProps {
block: BlockPayload;
@ -76,19 +77,31 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
return;
}
const timestamp = Date.now();
const newBlock: BlockPayload = {
chainUUID: block.chainUUID,
blockUUID: uuidv4(),
title: block.title,
description: block.description,
signedMessage: signature,
timestamp: Date.now(),
timestamp,
signatures: [{ address, signature }],
parentBlockUUID: block.blockUUID
};
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) {
throw new Error('Failed to send message to Waku network');
@ -168,16 +181,16 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
return (
<>
<Button onClick={() => setIsOpen(true)} disabled={alreadySigned}>
{alreadySigned ? 'Already Signed' : !address ? 'Connect Wallet' : 'Sign Chain'}
{alreadySigned ? 'Already Signed' : !address ? 'Connect Wallet' : 'Sign Book'}
</Button>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Sign Chain</DialogTitle>
<DialogTitle>Sign Book</DialogTitle>
<DialogDescription>
{alreadySigned
? 'You have already signed this chain.'
: 'Review the block details and sign to add your signature to the chain.'}
? 'You have already signed this book.'
: 'Review the block details and sign to add your signature to the book.'}
</DialogDescription>
</DialogHeader>
<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">
<CardContent className="flex flex-col items-center justify-center py-8 space-y-4">
<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>
</Card>
);
@ -39,11 +39,11 @@ const SignSharedChain: React.FC<SignSharedChainProps> = ({ chainsData, onChainUp
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>Chain Not Found</CardTitle>
<CardTitle>Book Not Found</CardTitle>
</CardHeader>
<CardContent>
<p className="mb-4">The requested chain or block could not be found.</p>
<Button onClick={() => navigate('/view')}>View All Chains</Button>
<p className="mb-4">The requested book or block could not be found.</p>
<Button onClick={() => navigate('/view')}>View All Books</Button>
</CardContent>
</Card>
);
@ -52,7 +52,7 @@ const SignSharedChain: React.FC<SignSharedChainProps> = ({ chainsData, onChainUp
return (
<Card className="w-full max-w-2xl mx-auto">
<CardHeader>
<CardTitle>Sign Shared Chain</CardTitle>
<CardTitle>Sign Shared Book</CardTitle>
</CardHeader>
<CardContent>
<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>
<DialogContent className="flex flex-col gap-4">
<DialogHeader>
<DialogTitle>Share Chain</DialogTitle>
<DialogTitle>Share this Book</DialogTitle>
<DialogDescription>
Share this chain with others to collect their signatures.
Share this book with others to collect their signatures.
</DialogDescription>
</DialogHeader>
@ -110,18 +110,18 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoad
<Card className="w-full max-w-4xl mx-auto">
<CardHeader>
<CardTitle>
Existing Chains
Existing Books
{isLoading && (
<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" />
Loading more chains...
Loading more books...
</span>
)}
</CardTitle>
</CardHeader>
<CardContent>
{rootBlocks.length === 0 && !isLoading ? (
<p>No chains found.</p>
<p>No books found.</p>
) : (
<ul className="space-y-4">
{rootBlocks.map((block) => renderBlock(block, 0))}

View File

@ -44,8 +44,8 @@ const ConnectionStatus: React.FC<ConnectionStatusProps> = ({ filter, store }) =>
return (
<Card className="fixed bottom-4 left-4 right-4 md:static md:bottom-auto md:left-auto p-2 bg-background/80 backdrop-blur-sm border shadow-lg z-50 md:z-auto">
<div className="flex flex-row justify-around md:justify-start md:gap-4">
<StatusIndicator status={filter} label="Filter" />
<StatusIndicator status={store} label="Store" />
<StatusIndicator status={filter} label="Connection" />
<StatusIndicator status={store} label="History" />
</div>
</Card>
);

View File

@ -86,14 +86,14 @@ const Header: React.FC<HeaderProps> = ({ wakuStatus }) => {
{!isWakuLoading && !wakuError && (
<>
<div className="flex items-center space-x-1">
<span className="hidden md:inline text-muted-foreground">Filter:</span>
<span className="hidden md:inline text-muted-foreground">Connection:</span>
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.filter)}`}></div>
</div>
<div className="flex items-center space-x-1">
<span className="hidden md:inline text-muted-foreground">Store:</span>
<span className="hidden md:inline text-muted-foreground">History:</span>
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.store)}`}></div>
</div>
<div className="flex items-center space-x-1">
<div className="flex items-center space-x-1 hidden">
<span className="hidden md:inline text-muted-foreground">Peers:</span>
{isWakuLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />

View File

@ -0,0 +1,263 @@
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: toInt(data.timestamp),
createdAt: toInt(data.timestamp),
seenTimestamp: toInt(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: toInt(data.timestamp),
createdAt: toInt(data.timestamp),
seenTimestamp: toInt(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: toInt(data.timestamp),
createdAt: toInt(data.timestamp),
seenTimestamp: toInt(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: toInt(data.timestamp),
createdAt: toInt(data.timestamp),
seenTimestamp: toInt(data.timestamp),
peerId: data.node.peerId.toString(),
contentTopic: data.decoder.contentTopic,
pubsubTopic: data.decoder.pubsubTopic,
ephemeral: false,
messageHash: uuidv4(),
errorMessage: "",
extraData: buildExtraData({
timeTaken: data.timeTaken
}),
}];
};
export function toInt(v: any): number {
return parseInt(v);
}

View File

@ -1,8 +1,15 @@
import { createEncoder, createDecoder, type LightNode, type CreateWakuNodeOptions } from "@waku/sdk";
import { createEncoder, createDecoder, type LightNode } from "@waku/sdk";
import { type CreateWakuNodeOptions } from "@waku/sdk";
import protobuf from 'protobufjs';
import { v4 as uuidv4 } from 'uuid';
import { Telemetry, fromFilter, fromStore, TelemetryType, buildExtraData, toInt } from "./telemetry";
export const WAKU_NODE_OPTIONS: CreateWakuNodeOptions = { defaultBootstrap: true, nodeToUse: {store: "/dns4/boot-01.do-ams3.status.staging.status.im/tcp/443/wss/p2p/16Uiu2HAmEqqio4UR1SWqAc7KY19t6qyDvtmyjreZpzUBJvb4u65R"} };
export const WAKU_NODE_OPTIONS: CreateWakuNodeOptions = {
defaultBootstrap: true,
nodeToUse: {
store: "/dns4/node-01.ac-cn-hongkong-c.waku.test.status.im/tcp/8000/wss/p2p/16Uiu2HAkzHaTP5JsUwfR9NR8Rj9HC24puS6ocaU8wze4QrXr9iXp"
}
};
export type Signature = {
address: `0x${string}`;
@ -64,35 +71,67 @@ export function createMessage({
}
export async function* getMessagesFromStore(node: LightNode) {
console.time("getMessagesFromStore")
const startTime = performance.now();
try {
for await (const messagePromises of node.store.queryGenerator([decoder])) {
const messages = await Promise.all(messagePromises);
for (const message of messages) {
console.log(message)
if (!message?.payload) continue;
const blockPayload = block.decode(message.payload) as unknown as BlockPayload;
blockPayload.signatures = blockPayload.signatures.map(s => JSON.parse(s as unknown as string) as Signature);
yield blockPayload;
}
}
} finally {
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) {
const endTime = performance.now();
const timeTaken = endTime - startTime;
Telemetry.push([{
type: TelemetryType.LIGHT_PUSH_FILTER,
protocol: "lightPush",
timestamp: toInt(startTime),
createdAt: toInt(startTime),
seenTimestamp: toInt(startTime),
peerId: node.peerId.toString(),
contentTopic: encoder.contentTopic,
pubsubTopic: encoder.pubsubTopic,
ephemeral: encoder.ephemeral,
messageHash: uuidv4(),
errorMessage: (e as Error)?.message ?? "Error during Store",
extraData: buildExtraData({ timeTaken }),
}]);
throw e;
}
}
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)
if (message.payload) {
const blockPayload = block.decode(message.payload) as unknown as BlockPayload;
blockPayload.signatures = blockPayload.signatures.map(s => JSON.parse(s as unknown as string) as Signature);
callback(blockPayload);
}
}
);
}, {forceUseAllPeers: false});
Telemetry.push(fromFilter({
result,
node,
decoder,
timestamp: Date.now(),
}));
const {error, subscription, results} = result;
console.log("results", results)
if (error) {