Перейти до вмісту
На сайт

Трансформації

Трансформація перетворює вміст події на тіло запиту, якого очікує отримувач. Змінюється лише тіло доставки; збережена подія лишається незмінною.

Вид трансформації, kind, визначає, як читати її текст.

kind Що це Коли брати
TEMPLATE JSON-документ, рядкові значення якого можуть містити вирази ${$.jsonpath}. Типовий вид. Отримувачу потрібні ті самі поля під іншими назвами або лише частина з них. Декларативний: без циклів і без несподіванок.
JAVASCRIPT Одна функція function handler(webhook), яка повертає тіло запиту й виконується в пісочниці. Структуру треба обчислити: пройти масив, додати поле залежно від значення, відформатувати дату, скасувати доставку.

Якщо kind не задано, це TEMPLATE. Трансформації, створені до появи kind, є шаблонами й такими лишаються.

TEMPLATE є JSON-документом, рядкові значення якого можуть містити JSONPath-вирази в плейсхолдерах ${…}, що обчислюються за вмістом події. Усе інше копіюється як є.

Шаблон
{
"id": "${$.orderId}",
"customer": {
"email": "${$.customer.email}",
"plan": "${$.subscription.plan_name}"
},
"amount_cents": "${$.amount}",
"note": "Order ${$.orderId} from Railhook"
}
Як записано Результат
Рядок, що складається рівно з одного виразу, "${$.amount}" Значення, на яке вказує вираз, зі своїм типом: число лишається числом, об’єкт об’єктом.
Вираз усередині іншого тексту, "Order ${$.orderId} from Railhook" Рядок із підставленим значенням.
Шлях, який нічого не знаходить null, якщо вираз займає все значення, і текст null усередині довшого рядка. Це не помилка.

Шаблон може прочитати вміст і переставити поля. Циклів, розгалужень, зовнішніх викликів і обчислень він не вміє; для цього є JAVASCRIPT.

Трансформація виду JAVASCRIPT є одним файлом з однією функцією:

Вигляд будь-якого скрипта
function handler(webhook) {
return { payload: { id: webhook.eventId } };
}

handler є єдиною назвою, яку викликає Railhook. Допоміжні функції й константи у файлі виконуються перед нею. Підійде будь-який запис, після якого handler є функцією, зокрема const handler = (webhook) => ({ payload: webhook.payload });.

Властивість Що це
payload Вміст події, уже розібраний. Об’єкт, масив або те, чим було тіло події.
eventType Тип події, а для вхідного вебхука тип події сервісу.
eventId Ідентифікатор події.
timestamp Коли подію було оголошено, рядком у форматі ISO-8601.
direction OUTGOING для доставки, INCOMING для пересилання.
url Адреса ендпоінта або призначення, на яку націлена ця спроба. Лише для читання.
headers Власні заголовки, налаштовані на ендпоінті чи призначенні, а не заголовки самого Railhook і не готовий запит. Підпис, ідентифікатори й порядковий номер обчислюються після скрипта й не показуються, тож скрипт не може злити підпис. У попередньому перегляді це ті customHeaders, які ви надіслали.
attemptNumber Котра це спроба, 1 для першої. У попередньому перегляді null, бо спроби немає.

Більше скрипт нічого не бачить.

Що функція має повернути

Section titled “Що функція має повернути”
Що повернути Дія
{ payload: … } payload серіалізується в JSON і стає тілом запиту, яке підписують і надсилають.
{ payload: …, headers: { … } } Ці заголовки додають до запиту. Значення зводяться до рядка; null і вкладений об’єкт відкидаються.
{ cancel: true, cancelReason: '…' } Нічого не надсилається, доставка завершується. Див. Коли скрипт скасовує доставку.

payload обов’язковий, якщо ви не скасовуєте доставку. Усе інше (відсутній return, рядок, об’єкт без payload і без cancel) є порушенням контракту, і спроба невдала.

Суворий режим, ECMAScript 2023, один файл, одна синхронна функція:

  • без модулів, без require, без import, без module.exports;
  • без async, без await, без промісів, на які можна було б зачекати;
  • без стану між запусками. Кожен запуск отримує чистий глобальний контекст, тож змінна верхнього рівня на наступній доставці знову має початкове значення.

console.log, console.info, console.warn, console.error і console.debug збираються й показуються поруч із результатом у «Студії трансформацій», зокрема рядки, записані до того, як скрипт кинув помилку.

Чого скрипт свідомо не може змінити

Section titled “Чого скрипт свідомо не може змінити”
  • Адресу. Railhook перевіряє адресу за правилами SSRF до запуску скрипта, тож скрипт, який міг би її переписати, обійшов би цю перевірку й міг би надіслати ваш підписаний трафік будь-куди. webhook.url можна прочитати; присвоєння йому нічого не робить.
  • HTTP-метод. Спроба завжди POST, і жодне повернене значення цього не змінює.
  • Секрети. Сховища секретів у скрипта немає. Для автентифікації в отримувача використовуйте облікові дані ендпоінта чи призначення (секрет підпису, клієнтський сертифікат mTLS, налаштування автентифікації призначення). Вони зашифровані в сховищі й застосовуються після скрипта, тож прочитати чи перезаписати їх він не може.
  • Власні заголовки Railhook. Спершу лягають заголовки скрипта, поверх них облікові дані та власні заголовки ендпоінта чи призначення; підпис обчислюється поза цим об’єднанням. Скрипт може додати заголовок, але не перекрити його.

Скрипт належить одній організації, але виконується на воркерах, спільних для всіх, тож отримує лише мову: без файлової системи, мережі, потоків і Java-класів.

До кожного запуску застосовують п’ять обмежень. Це налаштування розгортання, а не окремої трансформації, бо вони захищають воркери, і скрипт не може їх підняти чи перехопити.

Змінна Типово Що станеться, якщо вийти за межу
TRANSFORM_SCRIPT_TIMEOUT_MS 2000 Запуск зупиняють, спроба невдала з причиною TIMEOUT. Реальний час одного запуску.
TRANSFORM_SCRIPT_MAX_MEMORY_MB 128 Запуск зупиняють, спроба невдала з причиною MEMORY. Рахується вся виділена пам’ять, а не утримувана: прогнати гігабайт через збирач сміття коштує воркеру стільки ж, як його утримувати.
TRANSFORM_SCRIPT_MAX_OUTPUT_BYTES 1048576 Більше тіло не надсилається, а дає невдачу з причиною OUTPUT_TOO_LARGE, тож помилка називає скрипт, а не ендпоінт.
TRANSFORM_SCRIPT_MAX_CONSOLE_LINES 100 Нічого не ламається. Зайві рядки відкидаються, і результат про це каже.
TRANSFORM_SCRIPT_MAX_CHARS 65536 Скрипт відхиляють із причиною SOURCE_TOO_LARGE. API відхиляє довший скрипт ще під час збереження; це запасна перевірка.

Задайте їх у .env: див. довідник з конфігурації.

Кожен рядок покрито тестом.

Спроба Що станеться
require('fs'), readFully('/etc/passwd'), load('https://…/evil.js') Немає ні файлової системи, ні завантажувача. require і load не визначені.
fetch(…), new XMLHttpRequest(), new java.net.Socket(…) Мережевого API немає взагалі.
new Worker('x') Скрипт не може створити потік. Він виконується рівно на тому потоці, який його викликав, і не може пережити виклик.
Java.type('java.lang.System'), Packages.java.lang.System, new java.io.File(…) Жодного Java-класу назвати не можна, тож недосяжні ні репозиторій, ні пул з’єднань, ні рядок чужої організації.
process.env, Polyglot.eval(…) Немає ні середовища хоста, ні другої мови. process, Polyglot, java, Java, load і подібні глобальні об’єкти видаляються ще до вашого першого рядка, тож помилка звучить як «не визначено».
while (true) {} Коли минає тайм-аут, запуск скасовують ззовні, з іншого потоку.
while (true) { a.push(new Array(200000)) } Те саме, коли перевищено межу виділеної пам’яті.

Три речі, яких не вміє шаблон: перебудувати масив, додати поле за умовою, відформатувати дату. Тести паритету запускають саме цей скрипт і в попередньому перегляді, і у воркері.

Трансформація
function handler(webhook) {
// 1. Reshape an array — a template cannot loop.
var lines = webhook.payload.items.map(function (item) {
return { sku: item.sku, total: money(item.qty * item.price) };
});
var out = {
order: webhook.payload.id,
lines: lines,
value: money(lines.reduce(function (sum, line) { return sum + line.total; }, 0))
};
// 2. Add a field conditionally — a template cannot branch.
if (out.value >= 100) {
out.review = 'manual';
}
// 3. Format a date — a template cannot compute.
out.day = new Date(webhook.payload.placed_at).toISOString().slice(0, 10);
return { payload: out, headers: { 'X-Order-Value': out.value.toFixed(2) } };
}
function money(amount) {
return Math.round(amount * 100) / 100;
}
Вміст події
{
"id": "ord_9001",
"placed_at": "2026-09-20T14:05:09.123Z",
"items": [
{ "sku": "WIDGET", "qty": 2, "price": 19.99 },
{ "sku": "GADGET", "qty": 1, "price": 61.5 }
]
}
Доставлене тіло запиту
{
"order": "ord_9001",
"lines": [
{ "sku": "WIDGET", "total": 39.98 },
{ "sku": "GADGET", "total": 61.5 }
],
"value": 101.48,
"review": "manual",
"day": "2026-09-20"
}

І один доданий заголовок: X-Order-Value: 101.48.

До чого підключають трансформацію

Section titled “До чого підключають трансформацію”

Обидва види підключають в однакових місцях.

До чого Дія
Підписка, transformationId Збережена трансформація, яку застосовують до кожної доставки цієї підписки.
Підписка, payloadTemplate Вбудований шаблон; діє, коли transformationId не задано.
Правило, дія TRANSFORM Замінює трансформацію підписки для подій, які підпадають під правило. Якщо трансформацію задають кілька таких правил, діє останнє. Див. Правила.
Призначення, transformationId Збережена трансформація, яку застосовують до вхідних вебхуків перед пересиланням.
Призначення, payloadTransform Один JSONPath-вираз, наприклад $.data; діє, коли transformationId не задано.
Воркфлоу, вузол transform Змінює структуру даних усередині воркфлоу. Скрипт бачить вхідні дані кроку як свій payload і не має контексту доставки. Скрипт, який скасовує, завершує крок невдачею, бо доставки тут немає. Див. Воркфлоу.

Коли трансформація спрацьовує

Section titled “Коли трансформація спрацьовує”

Трансформацію застосовують у кожній спробі до підпису, тож підпис охоплює трансформовані байти, які отримує отримувач.

Збережену трансформацію завантажують у момент спроби, тож виправлення зламаної подіє на всі наступні спроби.

Чому скрипт не спрацював

Section titled “Чому скрипт не спрацював”

Невдалий скрипт записують у спробу як TRANSFORM_FAILED із причиною і, якщо відомо, номером рядка у вашому скрипті.

Причина Що це означає
SYNTAX Скрипт не скомпілювався.
CONTRACT Скрипт скомпілювався, але не оголошує handler або повернув не той конверт.
RUNTIME Скрипт виконався й кинув помилку або звернувся до того, чого в пісочниці немає.
TIMEOUT Скрипт ще виконувався, коли вичерпався час.
MEMORY Скрипт виділив більше пам’яті, ніж дозволено.
OUTPUT_TOO_LARGE Скрипт повернув тіло запиту більше за дозволене.
SOURCE_TOO_LARGE Сам скрипт довший, ніж рушій погоджується компілювати.
UNAVAILABLE Рушія немає в цьому розгортанні. Це проблема оператора.

Коли скрипт скасовує доставку

Section titled “Коли скрипт скасовує доставку”

{ cancel: true } не є невдачею:

  • нічого не надсилається, тож circuit breaker ендпоінта не зачіпається;
  • нічого не повторюється, бо той самий скрипт на тому самому вмісті знову скасував би доставку;
  • доставка на цьому завершується зі статусом CANCELLED, який не є ні SUCCESS, ні FAILED, і записується як CANCELLED_BY_TRANSFORMATION, а далі причина, яку назвав скрипт, або no reason given;
  • доставка не потрапляє до «Недоставлених повідомлень», які призначені для того, що людина ще може полагодити;
  • її рахує лічильник transform_cancelled_total, окремо від transform_failed_total.

Беріть це для трафіку, якого отримувач не має бачити: тестові події, клієнт, який відмовився від розсилки, подія, у якій нема чого надсилати.

function handler(webhook) {
if (webhook.payload.livemode === false) {
return { cancel: true, cancelReason: 'test traffic' };
}
return { payload: webhook.payload };
}

Як створити й перевірити трансформацію

Section titled “Як створити й перевірити трансформацію”
  1. Створіть її. Поля name і template обов’язкові; у template лежить або JSON-шаблон, або скрипт, і воно може мати до 65 536 символів. Якщо kind не задано, це TEMPLATE.

    Terminal window
    curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/transformations" \
    -H "X-API-Key: $API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"name":"Orders to CRM","kind":"JAVASCRIPT","template":"function handler(webhook) { return { payload: { id: webhook.payload.orderId } }; }"}'

    Перед збереженням скрипт компілюють і перевіряють, чи є в ньому handler, у тій самій пісочниці й з тими самими обмеженнями, що й доставка. Тож синтаксичну помилку відхиляють тут, із номером рядка, а не на кожній спробі.

  2. Перевірте результат на вставленому вмісті через POST /api/v1/projects/{projectId}/transform-preview. Надішліть inputPayload і або transformationId, або вбудований template разом із його kind. Для скрипта можна додати eventType, eventId, url і customHeaders, які він побачить у webhook.*. У відповіді є результат, заголовки, рядки console, тривалість запуску, а в разі невдачі причина й рядок. Нічого не доставляється.

  3. Змоделюйте доставку (пробний запуск) через POST /api/v1/projects/{projectId}/transform-preview/delivery-dry-run. Надішліть payload, трансформацію або payloadTemplate, а також endpointId. У відповіді є трансформоване тіло, заголовки й підпис, які було б надіслано, а також назва й версія трансформації. Нічого не надсилається.

  4. Підключіть трансформацію, задавши transformationId у підписці.

«Студія трансформацій» («Розробка» → «Студія трансформацій» або кнопка редагування на трансформації) виконує рушій воркера з тією самою пісочницею й обмеженнями, тож скрипт, який працює там, працює й на доставці. Вона має вкладки «Вхід», «Вихід», «Різниця» і «Консоль», запускається на ⌘/Ctrl+Enter, підсвічує рядок невдачі, а збереження публікує нову версію, як і через API.

Збереження зміненого шаблону чи скрипта публікує нову версію. Перейменування, редагування опису чи вимкнення цього не роблять. Зміна лише kind теж рахується зміною, бо той самий текст означає різне в двох мовах.

Кожну версію збережено з текстом, мовою, автором і часом публікації. У панелі номер версії у списку трансформацій відкриває історію: прочитати версію, порівняти дві або відновити одну. API має ті самі операції: див. Довідник API.

Відновлення не є скасуванням. Воно публікує стару версію як наступну й лишає все пізніше, тож історія того, що працювало, лишається правдивою. Нова версія записує, з якої версії її відновлено, а журнал аудиту отримує RESTORE. Відновлення повертає текст і мову разом; інакше шаблон, відновлений у трансформацію з JAVASCRIPT, ламав би кожну спробу.

Terminal window
curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/transformations/$TRANSFORMATION_ID/versions/2/restore" \
-H "X-API-Key: $API_KEY"

Трансформації, створені до появи історії версій, починають з однієї версії: свого поточного шаблону.