Трансформації
Трансформація перетворює вміст події на тіло запиту, якого очікує отримувач. Змінюється лише тіло доставки; збережена подія лишається незмінною.
Два види
Section titled “Два види”Вид трансформації, kind, визначає, як читати її текст.
kind |
Що це | Коли брати |
|---|---|---|
TEMPLATE |
JSON-документ, рядкові значення якого можуть містити вирази ${$.jsonpath}. Типовий вид. |
Отримувачу потрібні ті самі поля під іншими назвами або лише частина з них. Декларативний: без циклів і без несподіванок. |
JAVASCRIPT |
Одна функція function handler(webhook), яка повертає тіло запиту й виконується в пісочниці. |
Структуру треба обчислити: пройти масив, додати поле залежно від значення, відформатувати дату, скасувати доставку. |
Якщо kind не задано, це TEMPLATE. Трансформації, створені до появи kind, є шаблонами й такими лишаються.
Шаблон
Section titled “Шаблон”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
Section titled “Контракт JavaScript”Трансформація виду JAVASCRIPT є одним файлом з однією функцією:
function handler(webhook) { return { payload: { id: webhook.eventId } };}handler є єдиною назвою, яку викликає Railhook. Допоміжні функції й константи у файлі виконуються перед нею. Підійде будь-який запис, після якого handler є функцією, зокрема const handler = (webhook) => ({ payload: webhook.payload });.
Що є у webhook
Section titled “Що є у webhook”| Властивість | Що це |
|---|---|
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. Спершу лягають заголовки скрипта, поверх них облікові дані та власні заголовки ендпоінта чи призначення; підпис обчислюється поза цим об’єднанням. Скрипт може додати заголовок, але не перекрити його.
Пісочниця
Section titled “Пісочниця”Скрипт належить одній організації, але виконується на воркерах, спільних для всіх, тож отримує лише мову: без файлової системи, мережі, потоків і Java-класів.
Обмеження
Section titled “Обмеження”До кожного запуску застосовують п’ять обмежень. Це налаштування розгортання, а не окремої трансформації, бо вони захищають воркери, і скрипт не може їх підняти чи перехопити.
| Змінна | Типово | Що станеться, якщо вийти за межу |
|---|---|---|
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: див. довідник з конфігурації.
Що заборонено
Section titled “Що заборонено”Кожен рядок покрито тестом.
| Спроба | Що станеться |
|---|---|
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)) } |
Те саме, коли перевищено межу виділеної пам’яті. |
Розібраний приклад
Section titled “Розібраний приклад”Три речі, яких не вміє шаблон: перебудувати масив, додати поле за умовою, відформатувати дату. Тести паритету запускають саме цей скрипт і в попередньому перегляді, і у воркері.
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 “Як створити й перевірити трансформацію”-
Створіть її. Поля
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, у тій самій пісочниці й з тими самими обмеженнями, що й доставка. Тож синтаксичну помилку відхиляють тут, із номером рядка, а не на кожній спробі. -
Перевірте результат на вставленому вмісті через
POST /api/v1/projects/{projectId}/transform-preview. НадішлітьinputPayloadі абоtransformationId, або вбудованийtemplateразом із йогоkind. Для скрипта можна додатиeventType,eventId,urlіcustomHeaders, які він побачить уwebhook.*. У відповіді є результат, заголовки, рядкиconsole, тривалість запуску, а в разі невдачі причина й рядок. Нічого не доставляється. -
Змоделюйте доставку (пробний запуск) через
POST /api/v1/projects/{projectId}/transform-preview/delivery-dry-run. Надішлітьpayload, трансформацію абоpayloadTemplate, а такожendpointId. У відповіді є трансформоване тіло, заголовки й підпис, які було б надіслано, а також назва й версія трансформації. Нічого не надсилається. -
Підключіть трансформацію, задавши
transformationIdу підписці.
У панелі керування
Section titled “У панелі керування”«Студія трансформацій» («Розробка» → «Студія трансформацій» або кнопка редагування на трансформації) виконує рушій воркера з тією самою пісочницею й обмеженнями, тож скрипт, який працює там, працює й на доставці. Вона має вкладки «Вхід», «Вихід», «Різниця» і «Консоль», запускається на ⌘/Ctrl+Enter, підсвічує рядок невдачі, а збереження публікує нову версію, як і через API.
Історія версій
Section titled “Історія версій”Збереження зміненого шаблону чи скрипта публікує нову версію. Перейменування, редагування опису чи вимкнення цього не роблять. Зміна лише kind теж рахується зміною, бо той самий текст означає різне в двох мовах.
Кожну версію збережено з текстом, мовою, автором і часом публікації. У панелі номер версії у списку трансформацій відкриває історію: прочитати версію, порівняти дві або відновити одну. API має ті самі операції: див. Довідник API.
Відновлення не є скасуванням. Воно публікує стару версію як наступну й лишає все пізніше, тож історія того, що працювало, лишається правдивою. Нова версія записує, з якої версії її відновлено, а журнал аудиту отримує RESTORE. Відновлення повертає текст і мову разом; інакше шаблон, відновлений у трансформацію з JAVASCRIPT, ламав би кожну спробу.
curl -X POST "$RAILHOOK_URL/api/v1/projects/$PROJECT_ID/transformations/$TRANSFORMATION_ID/versions/2/restore" \ -H "X-API-Key: $API_KEY"Трансформації, створені до появи історії версій, починають з однієї версії: свого поточного шаблону.