LogoRobo.js

Custom engines

Build custom analytics providers or use multiple services at once.

The plugin supports any analytics service through the BaseEngine class. Extend it to integrate providers like Mixpanel, Amplitude, PostHog, or your own backend.

Creating a custom engine

Extend BaseEngine and implement the event() and view() methods:

my-engine.tsCustom engine
analytics.mjsRegister engine
src/engines/my-engine.ts
import { BaseEngine } from '@robojs/analytics'
import type { EventOptions, ViewOptions } from '@robojs/analytics'

export class MyEngine extends BaseEngine {
	private apiKey: string

	constructor(apiKey: string) {
		super()
		this.apiKey = apiKey
	}

	async event(name: string, options?: EventOptions): Promise<void> {
		await fetch('https://api.my-analytics.com/track', {
			method: 'POST',
			headers: {
				'Authorization': `Bearer ${this.apiKey}`,
				'Content-Type': 'application/json'
			},
			body: JSON.stringify({
				event: name,
				properties: options?.data,
				userId: options?.userId,
				sessionId: options?.sessionId,
				timestamp: Date.now()
			})
		})
	}

	async view(page: string, options?: ViewOptions): Promise<void> {
		await this.event('page_view', {
			...options,
			data: { page, ...((options?.data as Record<string, unknown>) ?? {}) }
		})
	}
}
src/engines/my-engine.js
import { BaseEngine } from '@robojs/analytics'

export class MyEngine extends BaseEngine {
	constructor(apiKey) {
		super()
		this.apiKey = apiKey
	}

	async event(name, options) {
		await fetch('https://api.my-analytics.com/track', {
			method: 'POST',
			headers: {
				'Authorization': `Bearer ${this.apiKey}`,
				'Content-Type': 'application/json'
			},
			body: JSON.stringify({
				event: name,
				properties: options?.data,
				userId: options?.userId,
				sessionId: options?.sessionId,
				timestamp: Date.now()
			})
		})
	}

	async view(page, options) {
		await this.event('page_view', {
			...options,
			data: { page, ...((options?.data) ?? {}) }
		})
	}
}

Register it in your plugin config:

config/plugins/robojs/analytics.mjs
import { MyEngine } from '../src/engines/my-engine.js'

export default {
	engine: new MyEngine(process.env.MY_ANALYTICS_KEY)
}
config/plugins/robojs/analytics.mjs
import { MyEngine } from '../src/engines/my-engine.js'

export default {
	engine: new MyEngine(process.env.MY_ANALYTICS_KEY)
}

The BaseEngine contract

abstract class BaseEngine {
	abstract event(name: string, options?: EventOptions): Promise<void> | void
	abstract view(page: string, options?: ViewOptions): Promise<void> | void
}
class BaseEngine {
	event(name, options) { /* abstract */ }
	view(page, options) { /* abstract */ }
}

Both methods can be synchronous or asynchronous. The Analytics singleton delegates directly to your implementation.

Best practices

  • Catch errors internally. Log failures instead of throwing. Unhandled errors from your engine can disrupt your bot's event flow.
  • Use the analytics logger. Import analyticsLogger for consistent log formatting:
import { analyticsLogger } from '@robojs/analytics/src/core/loggers.js'
analyticsLogger.debug('Event sent successfully')
import { analyticsLogger } from '@robojs/analytics/src/core/loggers.js'
analyticsLogger.debug('Event sent successfully')

Events are sent to all engines simultaneously using Promise.all(). Each engine handles its own errors independently.

Mixing built-in and custom engines

Combine built-in engines with your own:

config/plugins/robojs/analytics.mjs
import { GoogleAnalytics, ManyEngines } from '@robojs/analytics'
import { MyEngine } from '../src/engines/my-engine.js'

export default {
	engine: new ManyEngines(
		new GoogleAnalytics(),
		new MyEngine(process.env.MY_ANALYTICS_KEY)
	)
}
config/plugins/robojs/analytics.mjs
import { GoogleAnalytics, ManyEngines } from '@robojs/analytics'
import { MyEngine } from '../src/engines/my-engine.js'

export default {
	engine: new ManyEngines(
		new GoogleAnalytics(),
		new MyEngine(process.env.MY_ANALYTICS_KEY)
	)
}

Auto-detection shortcut

If you set both GOOGLE_ANALYTICS_MEASURE_ID and PLAUSIBLE_DOMAIN in your .env, the plugin automatically creates a ManyEngines wrapping both services. No config file needed.

When using ManyEngines, make sure each engine catches its own errors. If one engine throws (e.g., Plausible's 30-field limit), the Promise.all() rejects and may mask successful calls to other engines.

EventOptions reference

Both event() and view() accept an options object. Each engine decides which fields to use:

FieldTypeGoogleAnalyticsPlausibleCustom
sessionIdstringclient_idNot usedUp to you
userIdnumber | stringuser_idNot usedUp to you
dataunknownEvent paramsprops (max 30)Up to you
domainstringNot usedOverride domainUp to you
urlstringNot usedOverride URLUp to you
revenue{ currency, amount }Merged into paramsNot usedUp to you
actionstringNot usedNot usedUp to you
actionTypestringNot usedNot usedUp to you
labelstringNot usedNot usedUp to you
namestringNot usedNot usedUp to you
referrerstringNot usedNot usedUp to you
typestringNot usedNot usedUp to you

Fields marked "Up to you" are available for your custom engine to consume however you want.

Next Steps

On this page