customerio-node
Version:
A node client for the Customer.io event API. http://customer.io
439 lines (438 loc) • 20 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TrackClient = void 0;
const request_1 = __importDefault(require("./request"));
const regions_1 = require("./regions");
const utils_1 = require("./utils");
/**
* Client for the Customer.io Track API.
*
* Authenticates with a site id + API key (basic auth). Use this client to
* identify people, track events, manage devices, and merge profiles.
*
* Every method rejects with a {@link CustomerIORequestError} when the API
* returns a non-2xx status.
*
* @example
* ```ts
* import { TrackClient, RegionUS } from 'customerio-node';
*
* const cio = new TrackClient(siteId, apiKey, { region: RegionUS });
* await cio.identify('123', { email: 'a@example.com', plan: 'pro' });
* await cio.track('123', { name: 'signup' });
* ```
*/
class TrackClient {
siteid;
apikey;
defaults;
request;
trackRoot;
trackV2Root;
/**
* @param siteid Your Customer.io workspace site id.
* @param apikey Your Customer.io Track API key.
* @param defaults Optional overrides. Use `region` to select {@link RegionUS} or {@link RegionEU},
* `url` to point at a custom host (e.g. a mock server), `timeout` (ms, default `10000`), or any
* other fetch {@link RequestDefaults} field — notably `dispatcher` (an undici `Agent` /
* `ProxyAgent`) for proxies, custom TLS, or connection keep-alive.
* @throws If `region` is provided and is not a {@link Region} instance.
*/
constructor(siteid, apikey, defaults = {}) {
if (defaults.region && !(defaults.region instanceof regions_1.Region)) {
throw new Error('region must be one of Regions.US or Regions.EU');
}
this.siteid = siteid;
this.apikey = apikey;
this.defaults = { ...defaults, region: defaults.region || regions_1.RegionUS };
// `region`/`url` are SDK concerns (they select the host); strip them so the
// transport receives only fetch init. `retry` is handled by `Request`.
const { region: _region, url: _url, ...requestDefaults } = this.defaults;
this.request = new request_1.default({ siteid: this.siteid, apikey: this.apikey }, requestDefaults);
this.trackRoot = this.defaults.url ? this.defaults.url : this.defaults.region.trackUrl;
this.trackV2Root = this.defaults.url
? this.defaults.url.replace('/api/v1', '/api/v2')
: this.defaults.region.trackV2Url;
}
/**
* Create or update a person.
*
* To update an existing person's identifier (e.g. change their `id` or `email`),
* pass the `cio_id` prefixed with `cio_` as `customerId`.
*
* @param customerId The person's unique identifier (id, email, or `cio_<cio_id>`).
* @param data Attributes to set on the person. `email` is required if you intend to
* send email messages; `created_at` is required for time-based segmentation.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `customerId` is empty.
*/
identify(customerId, data = {}) {
if ((0, utils_1.isEmpty)(customerId)) {
throw new utils_1.MissingParamError('customerId');
}
return this.request.put(`${this.trackRoot}/customers/${encodeURIComponent(customerId)}`, data);
}
/**
* Permanently delete a person.
*
* This does not suppress the person — they can be re-added later. To prevent
* re-adding, use {@link TrackClient.suppress} instead.
*
* @param customerId The person's unique identifier.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `customerId` is empty.
*/
destroy(customerId) {
if ((0, utils_1.isEmpty)(customerId)) {
throw new utils_1.MissingParamError('customerId');
}
return this.request.destroy(`${this.trackRoot}/customers/${encodeURIComponent(customerId)}`);
}
/**
* Suppress a person. Suppressed people are deleted and cannot be re-added by
* subsequent `identify` calls. Pair with {@link TrackClient.unsuppress} to reverse.
*
* @param customerId The person's unique identifier.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `customerId` is empty.
*/
suppress(customerId) {
if ((0, utils_1.isEmpty)(customerId)) {
throw new utils_1.MissingParamError('customerId');
}
return this.request.post(`${this.trackRoot}/customers/${encodeURIComponent(customerId)}/suppress`);
}
/**
* Unsuppress a previously suppressed person.
*
* @param customerId The person's unique identifier.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `customerId` is empty.
*/
unsuppress(customerId) {
if ((0, utils_1.isEmpty)(customerId)) {
throw new utils_1.MissingParamError('customerId');
}
return this.request.post(`${this.trackRoot}/customers/${encodeURIComponent(customerId)}/unsuppress`);
}
/**
* Record an event for a known person.
*
* @param customerId The person's unique identifier.
* @param data Event payload. Must include a `name` string. Optionally include `data`
* (object of event attributes) and `type` to differentiate event vs. page events.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `customerId` or `data.name` is empty.
*/
track(customerId, data = {}) {
if ((0, utils_1.isEmpty)(customerId)) {
throw new utils_1.MissingParamError('customerId');
}
if ((0, utils_1.isEmpty)(data.name)) {
throw new utils_1.MissingParamError('data.name');
}
return this.request.post(`${this.trackRoot}/customers/${encodeURIComponent(customerId)}/events`, data);
}
/**
* Record an event for an anonymous (not-yet-identified) person.
*
* Anonymous events cannot trigger campaigns directly, but if event merging
* is enabled and you later `identify` the person with a matching
* `anonymous_id` attribute, prior anonymous events will be associated
* with the profile. Events associated within 72 hours can trigger campaigns.
*
* Pass an empty string for `anonymousId` to send an anonymous invite event
* (requires a `recipient` attribute in `data`).
*
* @param anonymousId Identifier for the anonymous person, or `''` for an invite event.
* @param data Event payload. Must include a `name` string.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `data.name` is empty.
*/
trackAnonymous(anonymousId, data = {}) {
if ((0, utils_1.isEmpty)(data.name)) {
throw new utils_1.MissingParamError('data.name');
}
let payload = { ...data };
if (!(0, utils_1.isEmpty)(anonymousId)) {
payload['anonymous_id'] = anonymousId;
}
return this.request.post(`${this.trackRoot}/events`, payload);
}
/**
* Record a page view event for a known person.
*
* @param customerId The person's unique identifier.
* @param path The page URL or path being viewed.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `customerId` or `path` is empty.
*/
trackPageView(customerId, path) {
if ((0, utils_1.isEmpty)(customerId)) {
throw new utils_1.MissingParamError('customerId');
}
if ((0, utils_1.isEmpty)(path)) {
throw new utils_1.MissingParamError('path');
}
return this.request.post(`${this.trackRoot}/customers/${encodeURIComponent(customerId)}/events`, {
type: 'page',
name: path,
});
}
/**
* Report a push notification lifecycle event (delivered, opened, converted).
*
* Used by integrations that want to feed delivery telemetry back to Customer.io
* for analytics and campaign optimization.
*
* @param data Push event payload (delivery_id, device_id, event, timestamp).
* @returns The parsed JSON response body.
*/
trackPush(data = {}) {
return this.request.post(`${this.trackRoot}/push/events`, data);
}
/**
* Register a device for push delivery to a person.
*
* @param customerId The person's unique identifier.
* @param device_id The platform-issued device/push token (e.g. APNs token, FCM token).
* @param platform Device platform — typically `"ios"` or `"android"`.
* @param data Optional device metadata. `last_used` (unix timestamp) is treated specially;
* any other keys are stored as device attributes.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `customerId`, `device_id`, or `platform` is empty.
*/
addDevice(customerId, device_id, platform, data = {}) {
if ((0, utils_1.isEmpty)(customerId)) {
throw new utils_1.MissingParamError('customerId');
}
if ((0, utils_1.isEmpty)(device_id)) {
throw new utils_1.MissingParamError('device_id');
}
if ((0, utils_1.isEmpty)(platform)) {
throw new utils_1.MissingParamError('platform');
}
let { last_used, ...attributes } = data;
return this.request.put(`${this.trackRoot}/customers/${encodeURIComponent(customerId)}/devices`, {
device: {
id: device_id,
platform,
...(last_used != null ? { last_used } : {}),
...(Object.keys(attributes).length && { attributes }),
},
});
}
/**
* Remove a device from a person.
*
* @param customerId The person's unique identifier.
* @param deviceToken The device/push token to remove.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `customerId` or `deviceToken` is empty.
*/
deleteDevice(customerId, deviceToken) {
if ((0, utils_1.isEmpty)(customerId)) {
throw new utils_1.MissingParamError('customerId');
}
if ((0, utils_1.isEmpty)(deviceToken)) {
throw new utils_1.MissingParamError('deviceToken');
}
return this.request.destroy(`${this.trackRoot}/customers/${encodeURIComponent(customerId)}/devices/${encodeURIComponent(deviceToken)}`);
}
/**
* Send a batch of Track API operations in a single request to the v2 batch endpoint.
*
* Each operation is a self-describing object (e.g. an identify, track, or
* delete payload). Use this to reduce request overhead for high-volume work.
*
* @param operations A non-empty array of batch operation objects.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `operations` is not a non-empty array.
*/
batch(operations) {
if (!Array.isArray(operations) || operations.length === 0) {
throw new utils_1.MissingParamError('operations');
}
return this.request.post(`${this.trackV2Root}/batch`, { batch: operations });
}
/**
* Merge two people. The secondary profile is permanently deleted and its
* attributes/events are merged into the primary.
*
* The identifier types are independent: you can merge by `email` into a
* profile identified by `id`, and so on.
*
* @param primaryIdType Type of identifier for the surviving profile.
* @param primaryId Identifier value for the surviving profile.
* @param secondaryIdType Type of identifier for the profile to be merged in.
* @param secondaryId Identifier value for the profile to be merged in.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If either `primaryId` or `secondaryId` is empty.
* @throws {Error} If either identifier type is not one of {@link IdentifierType}.
*/
mergeCustomers(primaryIdType, primaryId, secondaryIdType, secondaryId) {
if ((0, utils_1.isEmpty)(primaryId)) {
throw new utils_1.MissingParamError('primaryId');
}
if ((0, utils_1.isEmpty)(secondaryId)) {
throw new utils_1.MissingParamError('secondaryId');
}
if (!(0, utils_1.isIdentifierType)(primaryIdType) || !(0, utils_1.isIdentifierType)(secondaryIdType)) {
throw new Error('primaryIdType and secondaryIdType must be one of "id", "cio_id", or "email"');
}
return this.request.post(`${this.trackRoot}/merge_customers`, {
primary: {
[primaryIdType]: primaryId,
},
secondary: {
[secondaryIdType]: secondaryId,
},
});
}
/**
* Look up the data region your account belongs to.
*
* Confirms whether your workspace is hosted in the US or EU region and
* returns the base URL your other Track API calls should use.
*
* @returns The parsed JSON response body (`{ url, region, environment_id }`).
*/
getAccountRegion() {
return this.request.get(`${this.trackRoot}/accounts/region`);
}
/**
* Send a single self-describing Track operation to the v2 entity endpoint.
*
* This is the singular counterpart to {@link TrackClient.batch}: `operation`
* is the same shape as one element of a `batch` array — an identify, track,
* delete, merge, add/remove relationship, add/remove device, etc.
*
* @param operation A single batch-style operation object.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `operation` is not a non-empty object.
*/
entity(operation) {
if (operation == null || typeof operation !== 'object' || Array.isArray(operation)) {
throw new utils_1.MissingParamError('operation');
}
if (Object.keys(operation).length === 0) {
throw new utils_1.MissingParamError('operation');
}
return this.request.post(`${this.trackV2Root}/entity`, operation);
}
/**
* Add people to a manual segment.
*
* @param segmentId The manual segment's id.
* @param customerIds The identifiers of the people to add (1–1000). The value
* type must match `idType`; entries that don't match are ignored by the API.
* @param idType Which identifier kind the values in `customerIds` are. When
* omitted, the API uses its default (`id`).
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `segmentId` is empty or `customerIds` is not a non-empty array.
* @throws {Error} If `idType` is provided and is not a valid {@link IdentifierType}.
*/
addCustomersToSegment(segmentId, customerIds, idType) {
if ((0, utils_1.isEmpty)(segmentId)) {
throw new utils_1.MissingParamError('segmentId');
}
if (!Array.isArray(customerIds) || customerIds.length === 0) {
throw new utils_1.MissingParamError('customerIds');
}
if (idType != null && !(0, utils_1.isIdentifierType)(idType)) {
throw new Error('idType must be one of "id", "cio_id", or "email"');
}
const query = idType ? `?id_type=${idType}` : '';
return this.request.post(`${this.trackRoot}/segments/${encodeURIComponent(segmentId)}/add_customers${query}`, { ids: customerIds });
}
/**
* Remove people from a manual segment.
*
* @param segmentId The manual segment's id.
* @param customerIds The identifiers of the people to remove (1–1000). The value
* type must match `idType`; entries that don't match are ignored by the API.
* @param idType Which identifier kind the values in `customerIds` are. When
* omitted, the API uses its default (`id`).
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `segmentId` is empty or `customerIds` is not a non-empty array.
* @throws {Error} If `idType` is provided and is not a valid {@link IdentifierType}.
*/
removeCustomersFromSegment(segmentId, customerIds, idType) {
if ((0, utils_1.isEmpty)(segmentId)) {
throw new utils_1.MissingParamError('segmentId');
}
if (!Array.isArray(customerIds) || customerIds.length === 0) {
throw new utils_1.MissingParamError('customerIds');
}
if (idType != null && !(0, utils_1.isIdentifierType)(idType)) {
throw new Error('idType must be one of "id", "cio_id", or "email"');
}
const query = idType ? `?id_type=${idType}` : '';
return this.request.post(`${this.trackRoot}/segments/${encodeURIComponent(segmentId)}/remove_customers${query}`, { ids: customerIds });
}
/**
* Submit a form on behalf of a person.
*
* The `data` object holds the submitted form fields. It must contain exactly
* one identifier (`email` or `id`, depending on your workspace settings) so
* the submission can be attributed to a person; the person is created if they
* don't already exist.
*
* @param formId The form's id.
* @param data The submitted form fields, including the identifier.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `formId` is empty or `data` is not a non-empty object.
*/
submitForm(formId, data = {}) {
if ((0, utils_1.isEmpty)(formId)) {
throw new utils_1.MissingParamError('formId');
}
if (data == null || typeof data !== 'object' || Object.keys(data).length === 0) {
throw new utils_1.MissingParamError('data');
}
return this.request.post(`${this.trackRoot}/forms/${encodeURIComponent(formId)}/submit`, { data });
}
/**
* Report a delivery metric (open, click, bounce, etc.) back to Customer.io.
*
* Unlike {@link TrackClient.trackPush} (which targets push deliveries only via
* `/push/events`), this reports metrics for any channel — email, SMS, push,
* in-app, Slack, or webhook — to the `/metrics` endpoint. The valid `metric`
* values depend on the delivery's channel.
*
* @param data Metric payload. `delivery_id` is required; optionally include
* `metric`, `timestamp`, `recipient`, `reason`, and `href`.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `data.delivery_id` is empty.
*/
reportMetric(data = {}) {
if ((0, utils_1.isEmpty)(data.delivery_id)) {
throw new utils_1.MissingParamError('data.delivery_id');
}
return this.request.post(`${this.trackRoot}/metrics`, data);
}
/**
* Custom unsubscribe handling for a specific delivery.
*
* Sets (or clears) the recipient's `unsubscribed` attribute and attributes the
* change to the given delivery. This endpoint lives at the host root (not under
* `/api/v1`) and does not require authentication — the `deliveryId` itself is
* the secret.
*
* @param deliveryId The `CIO-Delivery-ID` of the message to unsubscribe from.
* @param unsubscribe When `true` (default) the person is unsubscribed; `false` resubscribes them.
* @returns The parsed JSON response body.
* @throws {MissingParamError} If `deliveryId` is empty.
*/
unsubscribe(deliveryId, unsubscribe = true) {
if ((0, utils_1.isEmpty)(deliveryId)) {
throw new utils_1.MissingParamError('deliveryId');
}
const host = this.trackRoot.replace('/api/v1', '');
return this.request.post(`${host}/unsubscribe/${encodeURIComponent(deliveryId)}`, { unsubscribe });
}
}
exports.TrackClient = TrackClient;