analytics-engine.ts•4.37 kB
export type MetricsBindings = {
	MCP_METRICS: AnalyticsEngineDataset
}
/**
 * Generic metrics event utilities
 * @description Wrapper for RA binding
 */
export class MetricsTracker {
	constructor(
		private wae: AnalyticsEngineDataset,
		private mcpServerInfo: {
			name: string
			version: string
		}
	) {}
	logEvent(event: MetricsEvent): void {
		try {
			event.serverInfo = this.mcpServerInfo
			let dataPoint = event.toDataPoint()
			this.wae.writeDataPoint(dataPoint)
		} catch (e) {
			console.error(`Failed to log metrics event, ${e}`)
		}
	}
}
/**
 * MetricsEvent
 *
 * Each event type is stored with a different indexId and has an associated class which
 * maps a more ergonomic event object to a ReadyAnalyticsEvent
 */
export abstract class MetricsEvent {
	public _serverInfo: { name: string; version: string } | undefined
	set serverInfo(serverInfo: { name: string; version: string }) {
		this._serverInfo = serverInfo
	}
	get serverInfo(): { name: string; version: string } {
		if (!this._serverInfo) {
			throw new Error('Server info not set')
		}
		return this._serverInfo
	}
	/**
	 * Output a valid AnalyticsEngineDataPoint. Use `mapBlobs` and `mapDoubles` to write well defined
	 * analytics engine datapoints. The first and second blob entries are reserved for the MCP server name and
	 * MCP server version.
	 */
	abstract toDataPoint(): AnalyticsEngineDataPoint
	mapBlobs(blobs: Blobs): Array<string | null> {
		if (blobs.blob1 || blobs.blob2) {
			throw new MetricsError(
				'Failed to map blobs, blob1 and blob2 are reserved for MCP server info'
			)
		}
		// add placeholder blobs, filled in by the MetricsTracker later
		blobs.blob1 = this.serverInfo.name
		blobs.blob2 = this.serverInfo.version
		const blobsArray = new Array(Object.keys(blobs).length)
		for (const [key, value] of Object.entries(blobs)) {
			const match = key.match(/^blob(\d+)$/)
			if (match === null || match.length < 2) {
				// we should never hit this because of the typedefinitions above,
				// but this error is for safety
				throw new MetricsError('Failed to map blobs, invalid key')
			}
			const index = parseInt(match[1], 10)
			if (isNaN(index)) {
				// we should never hit this because of the typedefinitions above,
				// but this esrror is for safety
				throw new MetricsError('Failed to map blobs, invalid index')
			}
			if (index - 1 >= blobsArray.length) {
				throw new MetricsError('Failed to map blobs, missing blob')
			}
			blobsArray[index - 1] = value
		}
		return blobsArray
	}
	mapDoubles(doubles: Doubles): number[] {
		const doublesArray = new Array(Object.keys(doubles).length)
		for (const [key, value] of Object.entries(doubles)) {
			const match = key.match(/^double(\d+)$/)
			if (match === null || match.length < 2) {
				// we should never hit this because of the typedefinitions above,
				// but this error is for safety
				throw new MetricsError(': Failed to map doubles, invalid key')
			}
			const index = parseInt(match[1], 10)
			if (isNaN(index)) {
				// we should never hit this because of the typedefinitions above,
				// but this error is for safety
				throw new MetricsError('Failed to map doubles, invalid index')
			}
			if (index - 1 >= doublesArray.length) {
				throw new MetricsError('Failed to map doubles, missing blob')
			}
			doublesArray[index - 1] = value
		}
		return doublesArray
	}
}
export enum MetricsEventIndexIds {
	AUTH_USER = 'auth_user',
	SESSION_START = 'session_start',
	TOOL_CALL = 'tool_call',
	CONTAINER_MANAGER = 'container_manager',
}
/**
 * Utility functions to map named blob/double objects to an array
 * We do this so we don't have to annotate `blob1`, `blob2`, etc in comments.
 *
 * I prefer this to just writing it in an array because it'll be easier to reference
 * later when we are writing ready analytics queries.
 *
 * IMO named tuples and raw arrays aren't as ergonomic to work with, but they require less of this code below
 */
type Range1To20 =
	| 1
	| 2
	| 3
	| 4
	| 5
	| 6
	| 7
	| 8
	| 9
	| 10
	| 11
	| 12
	| 13
	| 14
	| 15
	| 16
	| 17
	| 18
	| 19
	| 20
// blob1 and blob2 are reserved for server name and version
type Blobs = {
	[key in `blob${Range1To20}`]?: string | null
}
type Doubles = {
	[key in `double${Range1To20}`]?: number
}
export class MetricsError extends Error {
	constructor(message: string) {
		super(message)
		this.name = 'MetricsError'
	}
}