Node.js SDK
Публичный API серверной библиотеки ActionPulse для Node.js — опции конструктора, очередь, методы, классы ошибок и остановка процесса. Пакет не опубликован, рабочий путь сегодня — прямой HTTP.
На этой странице
В репозитории продукта есть серверная библиотека для Node.js. Она не опубликована, поэтому добавить её в зависимости проекта нельзя. Эта страница показывает её настоящий публичный API — чтобы вы понимали, что вас ждёт, — и называет рабочий путь для бэкенда сегодня.
Версия в репозитории — 0.1.0, она общая для всех трёх серверных библиотек
(Node.js, Go, Python). Пакет только ESM, требует Node.js 18 и выше, внешних
зависимостей у него нет.
Чем SDK отличается от прямого HTTP
Прямой HTTP — это один fetch и полный контроль. Библиотека добавляет к нему
то, что иначе пришлось бы написать самому:
| Что делает библиотека | Что это значит на практике |
|---|---|
| Проверяет контракт до отправки | неверное имя события бросает ValidationError, а не приходит в rejected |
| Держит ограниченную очередь в памяти | всплеск событий не превращается в лавину запросов |
| Сама режет батчи | соблюдает 500 элементов и 256 КБ на запрос без вашего участия |
| Повторяет только то, что стоит повторять | ошибка транспорта, 429 и 5xx, с учётом Retry-After |
| Пересчитывает индексы отказов | rejected[i].i указывает на позицию в вашем исходном вызове, а не в очередном HTTP-запросе |
| Санирует ошибки | сообщение не содержит ключ, тело запроса, идентификаторы и свойства |
Чего библиотека не делает: она не поднимает таймеров и ничего не отправляет
сама. Пока вы не вызвали flush() или close(), события лежат в памяти
процесса.
Клиент
import { ActionPulse } from '@actionpulse/server-node';
const serverKey = process.env.ACTIONPULSE_SERVER_KEY;
if (!serverKey) throw new Error('ACTIONPULSE_SERVER_KEY is required');
const pp = new ActionPulse({
endpoint: 'https://YOUR_ACTIONPULSE_HOST',
writeKey: serverKey,
appVersion: 'billing-2026.07.28',
batchSize: 100,
});
pp.track({
event: 'order_paid',
user_id: 'YOUR_USER_ID',
schema_version: 1,
props: { order_id: 'A-1024', amount_minor: 99000 },
});
const result = await pp.flush();
for (const item of result.rejected) {
console.error('event rejected', item.i, item.code);
}
Конструктор проверяет опции и не делает сетевых запросов: неверный адрес или
пустой ключ бросает ValidationError сразу, а не при первой отправке.
Обратите внимание на именование полей: опции конструктора — camelCase
(writeKey, batchSize), а поля события — те же snake_case, что и в HTTP
(user_id, schema_version, event_id). Это не опечатка: событие повторяет
контракт приёма буквально.
Опции конструктора
| Опция | По умолчанию | Что важно знать |
|---|---|---|
endpoint |
— | обязательно, абсолютный http/https адрес приёма |
writeKey |
— | обязательно, серверный ключ pp_sk_…, без переводов строки |
timeoutMs |
5000 |
таймаут одной попытки, снимается через AbortController |
queueCapacity |
1000 |
предел очереди в памяти |
batchSize |
100 |
элементов в одном запросе, не больше 500 |
maxRetries |
3 |
целое от 0 до 10; 0 означает «без повторов» |
baseBackoffMs |
100 |
начальная задержка |
maxBackoffMs |
2000 |
потолок задержки, не меньше baseBackoffMs |
appVersion |
— | до 128 печатных ASCII-байт, уходит в заголовок диагностики |
release |
— | то же, для идентификатора релиза |
logger |
— | (entry) => void, вызывается на каждой повторной попытке |
processor |
— | (event) => event или null, последний фильтр перед очередью |
fetch |
globalThis.fetch |
своя реализация запроса |
sleep |
встроенная | своя реализация ожидания для тестов |
Опции source и schemaVersion объявлены устаревшими. source игнорируется:
источник события выводится из типа ключа, а не из того, что указал вызывающий.
schemaVersion работает как значение по умолчанию для schema_version события.
processor вызывается для каждого события перед постановкой в очередь и
получает замороженный объект. Верните изменённое событие или null, чтобы
его выбросить. event_id и time восстанавливаются после вызова, поэтому
обработчик не может нарушить идемпотентность. Исключение внутри обработчика
превращается в ValidationError.
Методы
| Метод | Подпись | Поведение |
|---|---|---|
queueSize |
number (свойство) |
длина очереди |
track |
(event) => string |
ставит одно событие в очередь, возвращает его event_id |
batch |
(events) => string[] |
проверяет все события и ставит их все или ни одного |
group |
(input) => string |
ставит в очередь обычное событие group_identify |
identify |
(input) => Promise<object> |
отправляет сразу, без очереди и без повторов |
alias |
(input) => Promise<object> |
отправляет сразу; поле traits игнорируется |
flush |
() => Promise<TrackResponse> |
отправляет накопленное |
close |
() => Promise<TrackResponse> |
отправляет накопленное и закрывает клиент |
track и batch синхронные: они только ставят в очередь и бросают исключение,
если событие не проходит проверку или очередь заполнена. Никакого await для
них не нужно и никакого «незамеченного отказа промиса» они не создают.
identify и alias возвращают разобранное тело ответа. У этих операций нет
поля идемпотентности, поэтому они никогда не повторяются автоматически —
решение о повторе принимаете вы.
Одновременные вызовы flush() выстраиваются в очередь и не пересекаются, так
что вызывать его из нескольких мест безопасно.
Ошибки
| Класс | Когда | Что делать |
|---|---|---|
ValidationError |
локальная проверка контракта | исправить вызов |
QueueFullError |
очередь заполнена | увеличить queueCapacity или вызывать flush чаще |
TransportError |
сеть или таймаут, повторы исчерпаны | повторить позже |
APIError |
приём ответил не 202 |
смотреть status и code |
Все четыре наследуют ActionPulseError:
import { APIError, ActionPulseError } from '@actionpulse/server-node';
try {
await pp.flush();
} catch (error) {
if (error instanceof APIError && error.status === 401) {
console.error('actionpulse: credential rejected');
} else if (error instanceof ActionPulseError) {
console.error('actionpulse: delivery failed');
} else {
throw error;
}
}
APIError содержит только operation, status и канонический code из тела
ответа. Тела ответа, ключа и свойств события в сообщении нет намеренно: такую
ошибку можно писать в журнал целиком.
logger получает { operation, attempt, status } — и ничего больше. По
умолчанию журналирование выключено.
Остановка процесса
Автоматической отправки при выходе нет. Незакрытый клиент теряет всё, что осталось в очереди.
async function shutdown(signal) {
try {
await pp.close();
} catch (error) {
console.error('actionpulse close failed', error.message);
}
process.exit(signal === 'SIGINT' ? 130 : 0);
}
process.once('SIGTERM', () => void shutdown('SIGTERM'));
process.once('SIGINT', () => void shutdown('SIGINT'));
Важные детали:
close()сначала отправляет очередь, и только потом перестаёт принимать вызовы.- Если отправка не удалась, клиент остаётся открытым: можно починить связь и
повторить
close(). - Повторный
close()на уже закрытом клиенте возвращает пустой результат без исключения; вызов во время закрытия бросает ошибку. process.on('exit')для этого не подходит: асинхронная отправка в этом обработчике уже не выполнится. Нужен именно сигнальный обработчик.
Рабочий путь сегодня
Пока библиотека не опубликована, отправляйте события прямым HTTP: это тот же контракт, те же лимиты и те же коды отказов. Готовый пример на Node.js есть в отправке событий с бэкенда — там же перечислены поля события, коды отказов и правила повторов.
Если вы захотите позже перейти на библиотеку, полезно заранее сделать две вещи:
складывать event_id вместе с бизнес-фактом и держать отправку за одним
модулем в своём коде. Тогда замена транспорта не затронет вызовы.
Дальше: лимиты и квоты и доставка, повторы и дубли.