Create Your Own Destination
This guide provides the essentials for building a custom walkerOS destination.
What is a Destination?
A destination is a function that receives events from walkerOS and sends them to an external service, such as an analytics platform, an API, or a database.
The Destination Interface
A destination is an object that implements the Destination
interface. The most
important property is the push
function, which is called for every event.
interface Destination<Settings = unknown> {
config: {};
push: PushFn<Settings>;
type?: string;
init?: InitFn<Settings>;
on?(
event: 'consent' | 'session' | 'ready' | 'run',
context?: unknown,
): void | Promise<void>;
}
The push
function
The push
function is where you'll implement the logic to send the event to
your desired service. It receives the event
and a context
object containing
the destination's configuration.
type PushFn<Settings> = (
event: WalkerOS.Event,
context: {
config: {
settings?: Settings;
};
},
) => void;
Example: A Simple Webhook Destination
Here is an example of a simple destination that sends events to a webhook URL.
import type { Destination } from '@walkeros/core';
// 1. Define your settings interface
interface WebhookSettings {
url: string;
}
// 2. Create the destination object
export const destinationWebhook: Destination<WebhookSettings> = {
type: 'webhook',
config: {},
push(event, { config }) {
const { settings } = config;
// 3. Access your settings
if (!settings?.url) return;
// 4. Send the event
fetch(settings.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
}).catch(console.error);
},
};
The on
method (Optional)
The optional on
method allows your destination to respond to collector
lifecycle events. This is useful for handling consent changes, session
management, or cleanup tasks.
Available Events
consent
- Called when user consent changes, with consent state as contextsession
- Called when a new session starts, with session data as contextready
- Called when the collector is ready to process eventsrun
- Called when the collector starts or resumes processing
Example: Consent-Aware Destination
export const destinationWithConsent: Destination<WebhookSettings> = {
type: 'webhook-consent',
config: {},
on(event, context) {
if (event === 'consent') {
console.log('Consent updated:', context);
// React to consent changes - maybe clear cookies if consent withdrawn
}
},
push(event, { config }) {
console.log('Event:', event);
},
};
Using your destination
To use your custom destination, add it to the destinations
object in your
collector configuration.
import { startFlow } from '@walkeros/collector';
import { destinationWebhook } from './destinationWebhook';
const { elb } = await startFlow({
destinations: {
myWebhook: {
destination: destinationWebhook,
config: {
settings: {
url: 'https://api.example.com/events',
},
},
},
},
});
Advanced Example: Session Management
Here's a more advanced example that demonstrates session handling and cleanup:
export const destinationWithSession: Destination<WebhookSettings> = {
type: 'webhook-session',
config: {},
on(event, context) {
switch (event) {
case 'session':
// New session started
console.log('New session:', context);
// Could initialize session-specific tracking
break;
case 'consent':
// Handle consent changes
const consent = context as { marketing?: boolean; analytics?: boolean };
if (!consent?.marketing) {
// Clear marketing-related data if consent withdrawn
console.log('Marketing consent withdrawn, clearing data');
}
break;
case 'ready':
// Collector is ready
console.log('Starting destination services');
break;
case 'run':
// Collector resumed processing
console.log('Collector resumed, processing queued events');
break;
}
},
push(event, { config }) {
// Regular event processing
const { settings } = config;
if (!settings?.url) return;
fetch(settings.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
}).catch(console.error);
},
};
TypeScript Integration
To get full TypeScript support for your destination's configuration, you can
extend the WalkerOS.Destinations
interface.
// types.ts
import type { Destination } from '@walkeros/core';
import type { WebhookSettings } from './destinationWebhook';
declare global {
namespace WalkerOS {
interface Destinations {
webhook: Destination.Config<WebhookSettings>;
}
}
}
Environment Dependencies (Testing)
The env
parameter enables dependency injection for external APIs and SDKs. This allows you to test your destination logic without making actual API calls or requiring real browser globals.
Use Cases:
- Mock external SDKs (Google Analytics, Facebook Pixel, AWS SDK)
- Test without network requests
- Simulate different API responses
- Run tests in any environment (Node.js, browser, CI)
Defining an Environment
Define the external dependencies your destination needs:
// types.ts - Web destination
import type { DestinationWeb } from '@walkeros/web-core';
export interface Env extends DestinationWeb.Env {
window: {
gtag: (command: string, ...args: unknown[]) => void;
};
}
// types.ts - Server destination
import type { DestinationServer } from '@walkeros/server-core';
import type { BigQuery } from '@google-cloud/bigquery';
export interface Env extends DestinationServer.Env {
BigQuery?: typeof BigQuery;
}
Using Environment in Your Destination
Use the 3rd generic parameter for type safety, then access env in init
or push
:
import type { DestinationWeb } from '@walkeros/web-core';
import { getEnv } from '@walkeros/web-core';
interface Settings { /* ... */ }
interface Mapping { /* ... */ }
interface Env extends DestinationWeb.Env {
window: { customAPI: (event: string) => void };
}
// Add Env as 3rd generic parameter for proper typing
export const destination: DestinationWeb.Destination<Settings, Mapping, Env> = {
type: 'custom',
config: {},
async init({ config, env }) {
// Initialize SDK using env, falls back to real APIs
const { window } = getEnv(env);
window.customAPI('init');
return config;
},
push(event, { config, env }) {
const { window } = getEnv(env);
window.customAPI(event.name);
},
};
Creating Test Environments
Create reusable mock environments in an examples/env.ts
file:
// examples/env.ts
import type { Env } from '../types';
export const push: Env = {
window: {
customAPI: jest.fn(),
},
};
Export from your examples index:
// examples/index.ts
export * as env from './env';
Using in Tests
import { clone } from '@walkeros/core';
import { examples } from './index';
describe('My Destination', () => {
it('calls custom API', async () => {
// Clone the example env to avoid mutations
const testEnv = clone(examples.env.push);
await destination.push(event, { config, env: testEnv });
expect(testEnv.window.customAPI).toHaveBeenCalledWith('page view');
});
});
Key Points:
- Production: No
env
needed, uses real APIs (window, fetch, SDKs) - Testing: Provide
env
with mocks for isolated testing - Type Safety: 3rd generic parameter gives full autocomplete
- Fallback:
getEnv(env)
automatically uses real APIs if env not provided - Reusable: Store mock environments in
examples/env.ts
for consistency