customerio-node
Version:
A node client for the Customer.io event API. http://customer.io
211 lines (210 loc) • 9.93 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PipelinesClient = void 0;
const crypto_1 = require("crypto");
const request_1 = __importDefault(require("./request"));
const regions_1 = require("./regions");
const utils_1 = require("./utils");
const version_1 = require("./version");
/**
* Client for the Customer.io Pipelines (Data Pipelines) API.
*
* Authenticates with a Data Pipelines source write key. Use this client to send
* Segment-compatible identify/track/page/screen/group/alias events, one at a
* time or batched. Payloads use camelCase (`userId`, `anonymousId`, `groupId`,
* `previousId`), matching the Pipelines wire format.
*
* `messageId`, `timestamp`, and `context.library` are auto-filled on every call
* when you don't supply them; per-call values always win.
*
* Every method rejects with a {@link CustomerIORequestError} when the API
* returns a non-2xx status.
*
* @example
* ```ts
* import { PipelinesClient, RegionUS } from 'customerio-node';
*
* const cdp = new PipelinesClient(writeKey, { region: RegionUS });
* await cdp.identify({ userId: '123', traits: { email: 'a@example.com' } });
* await cdp.track({ userId: '123', event: 'Signed Up' });
* ```
*/
class PipelinesClient {
writeKey;
defaults;
request;
pipelinesRoot;
autoContext;
/**
* @param writeKey Your Data Pipelines source write key (Data Pipelines -> Sources).
* @param defaults Optional overrides. Use `region` to select {@link RegionUS} or
* {@link RegionEU}, `url` to point at a custom host, `strictMode` to have the API
* validate payloads and return real HTTP errors, or `defaultContext` to merge a
* base `context` under every call. Accepts any fetch {@link RequestDefaults} field too
* (e.g. `timeout`, or `dispatcher` for proxies / custom TLS / keep-alive).
* @throws {MissingParamError} If `writeKey` is empty.
* @throws {Error} If `region` is provided and is not a {@link Region} instance.
*/
constructor(writeKey, defaults = {}) {
if ((0, utils_1.isEmpty)(writeKey)) {
throw new utils_1.MissingParamError('writeKey');
}
if (defaults.region && !(defaults.region instanceof regions_1.Region)) {
throw new Error('region must be one of Regions.US or Regions.EU');
}
this.writeKey = writeKey;
// Strict mode rides into `defaults.headers` so the `CIORequest.options()`
// passthrough delivers it on every request. Existing headers on the
// incoming defaults are preserved.
const headers = defaults.strictMode ? { ...(defaults.headers ?? {}), 'X-Strict-Mode': '1' } : defaults.headers;
this.defaults = { ...defaults, headers, region: defaults.region || regions_1.RegionUS };
// The Pipelines API authenticates with HTTP Basic where the write key is
// the username and the password is blank. The existing `CIORequest`
// BasicAuth helper builds `${siteid}:${apikey}` — we map the write key
// into `siteid` and leave `apikey` empty so the encoded credential is
// exactly `base64(writeKey:)`. The field names are an internal artifact
// and never leak through to the public API.
//
// Strip the SDK-only keys; the remainder (the computed `headers`, plus any
// `dispatcher`/`keepalive`/`timeout`/retry the caller supplied) is fetch
// init for the transport.
const { region: _region, url: _url, strictMode: _strictMode, defaultContext: _defaultContext, ...requestDefaults } = this.defaults;
this.request = new request_1.default({ siteid: writeKey, apikey: '' }, requestDefaults);
this.pipelinesRoot = this.defaults.url ? this.defaults.url : this.defaults.region.pipelinesUrl;
this.autoContext = {
...(this.defaults.defaultContext ?? {}),
library: { name: 'customerio-node', version: version_1.version },
};
}
/**
* Identify a person, creating or updating their traits.
*
* @param payload Identify payload. Must include `userId` or `anonymousId`;
* `traits` carries the person's attributes.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If neither `userId` nor `anonymousId` is present.
*/
identify(payload) {
if ((0, utils_1.isEmpty)(payload?.userId) && (0, utils_1.isEmpty)(payload?.anonymousId)) {
throw new utils_1.MissingParamError('userId or anonymousId');
}
return this.request.post(`${this.pipelinesRoot}/identify`, this.envelope(payload));
}
/**
* Record an event for a person.
*
* @param payload Track payload. Must include `userId` or `anonymousId` and a
* non-empty `event` name; `properties` carries event attributes.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `userId`/`anonymousId` or `event` is missing.
*/
track(payload) {
if ((0, utils_1.isEmpty)(payload?.userId) && (0, utils_1.isEmpty)(payload?.anonymousId)) {
throw new utils_1.MissingParamError('userId or anonymousId');
}
if ((0, utils_1.isEmpty)(payload?.event)) {
throw new utils_1.MissingParamError('event');
}
return this.request.post(`${this.pipelinesRoot}/track`, this.envelope(payload));
}
/**
* Record a page view (web) for a person.
*
* @param payload Page payload. Must include `userId` or `anonymousId`;
* optional `name`, `category`, and `properties`.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If neither `userId` nor `anonymousId` is present.
*/
page(payload) {
if ((0, utils_1.isEmpty)(payload?.userId) && (0, utils_1.isEmpty)(payload?.anonymousId)) {
throw new utils_1.MissingParamError('userId or anonymousId');
}
return this.request.post(`${this.pipelinesRoot}/page`, this.envelope(payload));
}
/**
* Record a screen view (mobile) for a person.
*
* @param payload Screen payload. Must include `userId` or `anonymousId`;
* optional `name`, `category`, and `properties`.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If neither `userId` nor `anonymousId` is present.
*/
screen(payload) {
if ((0, utils_1.isEmpty)(payload?.userId) && (0, utils_1.isEmpty)(payload?.anonymousId)) {
throw new utils_1.MissingParamError('userId or anonymousId');
}
return this.request.post(`${this.pipelinesRoot}/screen`, this.envelope(payload));
}
/**
* Associate a person with a group (e.g. an account or organization).
*
* @param payload Group payload. Must include `userId` or `anonymousId` and a
* non-empty `groupId`; `traits` carries group attributes.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `userId`/`anonymousId` or `groupId` is missing.
*/
group(payload) {
if ((0, utils_1.isEmpty)(payload?.userId) && (0, utils_1.isEmpty)(payload?.anonymousId)) {
throw new utils_1.MissingParamError('userId or anonymousId');
}
if ((0, utils_1.isEmpty)(payload?.groupId)) {
throw new utils_1.MissingParamError('groupId');
}
return this.request.post(`${this.pipelinesRoot}/group`, this.envelope(payload));
}
/**
* Merge two identities by aliasing a previous id to a user id.
*
* @param payload Alias payload. Must include both `userId` and `previousId`.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `userId` or `previousId` is missing.
*/
alias(payload) {
if ((0, utils_1.isEmpty)(payload?.userId)) {
throw new utils_1.MissingParamError('userId');
}
if ((0, utils_1.isEmpty)(payload?.previousId)) {
throw new utils_1.MissingParamError('previousId');
}
return this.request.post(`${this.pipelinesRoot}/alias`, this.envelope(payload));
}
/**
* Send multiple events in a single request. Each item carries a `type`
* discriminator (`identify`, `track`, `page`, `screen`, `group`, or `alias`)
* and is enveloped (auto-filled `messageId`/`timestamp`/`context`) individually.
*
* @param items A non-empty array of {@link BatchItem} entries.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `items` is not a non-empty array.
*/
batch(items) {
if (!Array.isArray(items) || items.length === 0) {
throw new utils_1.MissingParamError('items');
}
const batch = items.map((item) => ({ ...this.envelope(item), type: item.type }));
return this.request.post(`${this.pipelinesRoot}/batch`, { batch });
}
// Auto-fills `messageId`, `timestamp`, and `context.library` when the caller
// hasn't supplied them. Per-call values always win over the auto-fill and
// over `defaultContext`.
envelope(payload) {
const merged = {
...this.autoContext,
...(payload.context ?? {}),
};
// `context.library` is a sub-object — preserve per-call override
// explicitly so a partial `context: { ip: '...' }` doesn't drop the
// auto-filled library identifier.
merged.library = payload.context?.library ?? this.autoContext.library;
return {
...payload,
messageId: payload.messageId ?? (0, crypto_1.randomUUID)(),
timestamp: payload.timestamp ?? new Date().toISOString(),
context: merged,
};
}
}
exports.PipelinesClient = PipelinesClient;