log message size in worker analytics (#3274)

Adds logging of message size in worker analytics.

This also adds the environment to worker analytics as `blob2`. We need
this because previously, all the analytics from all environments were
going to the same place with no ability to tell them apart, which means
we can't easily compare analytics on e.g. a particular PR.

This means that all the other blobs get shifted along one, so we won't
be able to query across the boundary of when this gets released for
those properties. I think this is fine though - it's things like
`roomId` that I don't think we were querying on anyway.

You can query the analytics through grafana - [docs
here](https://www.notion.so/tldraw/How-to-11fce2ed0be5480bb8e711c7ff1a0488?pvs=4#a66fae7bfcfe4ffe9d5348504598c6a0)

### Change Type
- [x] `internal` — Does not affect user-facing stuff
- [x] `chore` — Updating dependencies, other boring stuff
This commit is contained in:
alex 2024-03-27 11:33:47 +00:00 committed by GitHub
parent d45d77bedf
commit 408a269114
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 84 additions and 45 deletions

View file

@ -5,12 +5,13 @@ import { SupabaseClient } from '@supabase/supabase-js'
import { import {
RoomSnapshot, RoomSnapshot,
TLServer, TLServer,
TLServerEvent,
TLSyncRoom, TLSyncRoom,
type DBLoadResult, type DBLoadResult,
type PersistedRoomSnapshotForSupabase, type PersistedRoomSnapshotForSupabase,
type RoomState, type RoomState,
} from '@tldraw/tlsync' } from '@tldraw/tlsync'
import { assert, assertExists } from '@tldraw/utils' import { assert, assertExists, exhaustiveSwitchError } from '@tldraw/utils'
import { IRequest, Router } from 'itty-router' import { IRequest, Router } from 'itty-router'
import Toucan from 'toucan-js' import Toucan from 'toucan-js'
import { AlarmScheduler } from './AlarmScheduler' import { AlarmScheduler } from './AlarmScheduler'
@ -255,38 +256,42 @@ export class TLDrawDurableObject extends TLServer {
return new Response(null, { status: 101, webSocket: clientWebSocket }) return new Response(null, { status: 101, webSocket: clientWebSocket })
} }
logEvent( private writeEvent(
event: name: string,
| { { blobs, indexes, doubles }: { blobs?: string[]; indexes?: [string]; doubles?: number[] }
type: 'client'
roomId: string
name: string
clientId: string
instanceId: string
localClientId: string
}
| {
type: 'room'
roomId: string
name: string
}
) { ) {
this.measure?.writeDataPoint({
blobs: [name, this.env.WORKER_NAME ?? 'development-tldraw-multiplayer', ...(blobs ?? [])],
doubles,
indexes,
})
}
logEvent(event: TLServerEvent) {
switch (event.type) { switch (event.type) {
case 'room': { case 'room': {
this.measure?.writeDataPoint({ // we would add user/connection ids here if we could
blobs: [event.name, event.roomId], // we would add user/connection ids here if we could this.writeEvent(event.name, { blobs: [event.roomId] })
})
break break
} }
case 'client': { case 'client': {
this.measure?.writeDataPoint({ // we would add user/connection ids here if we could
blobs: [event.name, event.roomId, event.clientId, event.instanceId], // we would add user/connection ids here if we could this.writeEvent(event.name, {
blobs: [event.roomId, event.clientId, event.instanceId],
indexes: [event.localClientId], indexes: [event.localClientId],
}) })
break break
} }
case 'send_message': {
this.writeEvent(event.type, {
blobs: [event.roomId, event.messageType],
doubles: [event.messageLength],
})
break
}
default: {
exhaustiveSwitchError(event)
}
} }
} }

View file

@ -26,4 +26,5 @@ export interface Environment {
TLDRAW_ENV: string | undefined TLDRAW_ENV: string | undefined
SENTRY_DSN: string | undefined SENTRY_DSN: string | undefined
IS_LOCAL: string | undefined IS_LOCAL: string | undefined
WORKER_NAME: string | undefined
} }

View file

@ -1,4 +1,4 @@
export { TLServer, type DBLoadResult } from './lib/TLServer' export { TLServer, type DBLoadResult, type TLServerEvent } from './lib/TLServer'
export { export {
TLSyncClient, TLSyncClient,
type TLPersistentClientSocket, type TLPersistentClientSocket,

View file

@ -3,18 +3,25 @@ import ws from 'ws'
import { TLRoomSocket } from './TLSyncRoom' import { TLRoomSocket } from './TLSyncRoom'
import { TLSocketServerSentEvent } from './protocol' import { TLSocketServerSentEvent } from './protocol'
type ServerSocketAdapterOptions = {
readonly ws: WebSocket | ws.WebSocket
readonly logSendMessage: (type: string, size: number) => void
}
/** @public */ /** @public */
export class ServerSocketAdapter<R extends UnknownRecord> implements TLRoomSocket<R> { export class ServerSocketAdapter<R extends UnknownRecord> implements TLRoomSocket<R> {
constructor(public readonly ws: WebSocket | ws.WebSocket) {} constructor(public readonly opts: ServerSocketAdapterOptions) {}
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
get isOpen(): boolean { get isOpen(): boolean {
return this.ws.readyState === 1 // ready state open return this.opts.ws.readyState === 1 // ready state open
} }
// see TLRoomSocket for details on why this accepts a union and not just arrays // see TLRoomSocket for details on why this accepts a union and not just arrays
sendMessage(msg: TLSocketServerSentEvent<R>) { sendMessage(msg: TLSocketServerSentEvent<R>) {
this.ws.send(JSON.stringify(msg)) const message = JSON.stringify(msg)
this.opts.logSendMessage(msg.type, message.length)
this.opts.ws.send(message)
} }
close() { close() {
this.ws.close() this.opts.ws.close()
} }
} }

View file

@ -20,6 +20,32 @@ export type DBLoadResult =
type: 'room_not_found' type: 'room_not_found'
} }
export type TLServerEvent =
| {
type: 'client'
name: 'room_create' | 'room_reopen' | 'enter' | 'leave' | 'last_out'
roomId: string
clientId: string
instanceId: string
localClientId: string
}
| {
type: 'room'
name:
| 'failed_load_from_db'
| 'failed_persist_to_db'
| 'room_empty'
| 'fail_persist'
| 'room_start'
roomId: string
}
| {
type: 'send_message'
roomId: string
messageType: string
messageLength: number
}
/** /**
* This class manages rooms for a websocket server. * This class manages rooms for a websocket server.
* *
@ -116,7 +142,19 @@ export abstract class TLServer {
const clientId = nanoid() const clientId = nanoid()
const [roomState, roomOpenKind] = await this.getInitialRoomState(persistenceKey) const [roomState, roomOpenKind] = await this.getInitialRoomState(persistenceKey)
roomState.room.handleNewSession(sessionKey, new ServerSocketAdapter(socket)) roomState.room.handleNewSession(
sessionKey,
new ServerSocketAdapter({
ws: socket,
logSendMessage: (messageType, messageLength) =>
this.logEvent({
type: 'send_message',
roomId: persistenceKey,
messageType,
messageLength,
}),
})
)
if (roomOpenKind === 'new' || roomOpenKind === 'reopen') { if (roomOpenKind === 'new' || roomOpenKind === 'reopen') {
// Record that the room is now active // Record that the room is now active
@ -223,22 +261,7 @@ export abstract class TLServer {
* @param event - The event to log. * @param event - The event to log.
* @public * @public
*/ */
abstract logEvent( abstract logEvent(event: TLServerEvent): void
event:
| {
type: 'client'
roomId: string
name: string
clientId: string
instanceId: string
localClientId: string
}
| {
type: 'room'
roomId: string
name: string
}
): void
/** /**
* Get a room by its id. * Get a room by its id.

View file

@ -185,6 +185,7 @@ name = "${previewId}-tldraw-assets"`
let didUpdateTlsyncWorker = false let didUpdateTlsyncWorker = false
async function deployTlsyncWorker({ dryRun }: { dryRun: boolean }) { async function deployTlsyncWorker({ dryRun }: { dryRun: boolean }) {
const workerId = `${previewId ?? env.TLDRAW_ENV}-tldraw-multiplayer`
if (previewId && !didUpdateTlsyncWorker) { if (previewId && !didUpdateTlsyncWorker) {
appendFileSync( appendFileSync(
join(worker, 'wrangler.toml'), join(worker, 'wrangler.toml'),
@ -212,6 +213,8 @@ name = "${previewId}-tldraw-multiplayer"`
`TLDRAW_ENV:${env.TLDRAW_ENV}`, `TLDRAW_ENV:${env.TLDRAW_ENV}`,
'--var', '--var',
`APP_ORIGIN:${env.APP_ORIGIN}`, `APP_ORIGIN:${env.APP_ORIGIN}`,
'--var',
`WORKER_NAME:${workerId}`,
], ],
{ {
pwd: worker, pwd: worker,