Skip to content

FY Recommendations IFX — Handlers Guide

FY Recommendations IFX — Handlers Guide

This document walks through the three basic handlers shipped in this bundle, how they are wired into the Arc XP IFX runtime, what variables they read, and how to take the bundle from a fresh clone to an installed integration.


1. What the bundle does

The bundle subscribes to a small set of Arc XP CMS events and forwards each story mutation to a downstream content collector. The flow is:

Arc XP CMS ──story:* events──▶ IFX runtime ──HTTPS──▶ Collector API
(this bundle)

For every story event the IFX runtime hands us, we:

  1. Validate the ANS document on the event.
  2. Resolve the website(s) the story belongs to.
  3. Map the ANS payload into the collector’s content schema.
  4. POST the mapped payload to the collector, once per website.

The handlers are idempotent: the same event delivered twice produces the same payload, so the IFX runtime’s at-least-once retry semantics are safe.


2. Files at a glance

PathRole
src/eventsRouter.jsonMaps Arc XP event names to handler names.
src/eventsHandlers.jsAggregates the handler modules for the IFX SDK.
src/eventsHandlers/storyPublishHandler.jsUpsert path: publish / republish / update / create.
src/eventsHandlers/storyUnpublishHandler.jsDelete path: story unpublished.
src/eventsHandlers/storyDeleteHandler.jsDelete path: story removed.
src/lib/ansToFy.jsANS → collector schema mapping.
src/lib/collectorClient.jsThin HTTPS client around the collector content endpoint.
src/utils/namedLogger.jsStructured logger used by every handler.
test/replayIdempotency.test.jsAsserts replay safety and the basic payload shape.

3. Variables used by the bundle

The handlers themselves take no static configuration. The collector client and the logger read everything from the runtime environment.

VariableRead byRequiredPurpose
FY_COLLECTOR_BASE_URLsrc/lib/collectorClient.jsyesBase URL of the collector. The client appends the content path to it.
FY_COLLECTOR_PATsrc/lib/collectorClient.jsyesBearer credential sent as X-Api-Key on every outbound request.
LOG_LEVELsrc/utils/namedLogger.jsnoPino log level. Accepts trace/debug/info/warn/error/fatal, or disabled.
GITHUB_TOKENnpm install (build time only)yesUsed by .npmrc to pull the Arc XP IFX SDK from GitHub Packages. Not read at runtime.

The collector client refuses to start a request if FY_COLLECTOR_BASE_URL or FY_COLLECTOR_PAT is missing — it throws synchronously before any HTTP call. That deliberately surfaces misconfiguration on the very first invocation rather than silently dropping events.

Per-environment values live in gitignored files:

  • .env — used by the local testing server.
  • .env.sandbox — read by createSandboxBundle.
  • .env.production — read by createProdBundle.

None of these files should be committed.


4. The router

src/eventsRouter.json is the contract between the IFX runtime and our handlers. The SDK reads this file at bundle-generation time and dispatches each incoming event to the named handler.

{
"storyPublishHandler": ["story:create", "story:first-publish", "story:republish", "story:update"],
"storyUnpublishHandler": ["story:unpublish"],
"storyDeleteHandler": ["story:delete"]
}

Two design points worth calling out:

  • story:create is routed to the publish handler. Some tenants emit a create before the first publish; treating it as an upsert keeps the catalogue in sync regardless of which event arrives first.
  • unpublish and delete share a code path but not a handler. They produce the same collector payload (action: 'delete'), but live in separate handlers so logging, metrics, and future divergence remain straightforward.

5. The handlers, step by step

All three handlers follow the same five-step shape. Reading one is enough to understand the others.

5.1 storyPublishHandler

const storyPublishHandler = async (event) => {
const ans = event && event.body;
if (!ans || !ans._id) {
throw new Error('missing ANS document or _id');
}
const websites = getWebsiteIds(ans);
if (websites.length === 0) {
throw new Error('no websites in event');
}
const logger = namedLogger('storyPublishHandler').child({ arcStoryId: ans._id });
logger.info({ message: 'Received story publish event', websites });
for (const website of websites) {
await postContent(ansToUpsert(ans, website));
}
};

Step by step:

  1. Pull the ANS body. The IFX runtime hands the handler a wrapper object; the actual ANS document is on event.body.
  2. Validate. If the body is missing or has no _id we throw. The runtime will see the failure and retry — this is intentional, because a story with no _id cannot be upserted idempotently anyway.
  3. Resolve websites. getWebsiteIds(ans) prefers ans._website_ids and falls back to Object.keys(ans.websites). Throwing on an empty result keeps us from silently dropping events that look valid but target nothing.
  4. Log with context. A child logger pins arcStoryId onto every log line so the entire lifecycle of one story is greppable in aggregated logs.
  5. Fan out per website. One postContent call per site. The loop is sequential on purpose: the 60-second IFX handler budget is plenty for the small number of sites a story usually targets, and serializing keeps error semantics simple — if one site fails, the runtime retries the whole event, and idempotency makes that safe.

5.2 storyUnpublishHandler and storyDeleteHandler

These are structurally identical to the publish handler. The only differences:

  • They call ansToDelete(ans, website) instead of ansToUpsert(ans, website).
  • They log under their own logger name so the two paths are distinguishable.

The collector receives { action: 'delete', item_id, site_id } and removes the item from the catalogue for that site.


6. The collector client

src/lib/collectorClient.js is a single-purpose HTTPS client.

  • It reads FY_COLLECTOR_BASE_URL and FY_COLLECTOR_PAT on every call. Reading per-call (rather than at module load) lets the bundle pick up env changes between invocations in long-lived runtime processes.
  • It POSTs to a fixed content path (CONTENT_PATH constant) under the base URL.
  • It sets three headers:
    • Content-Type: application/json
    • Arc-Priority: ingestion — the documented Arc XP header that prevents IFX feedback loops on story:update.
    • X-Api-Key: <FY_COLLECTOR_PAT>
  • It uses a 30-second axios timeout, leaving headroom under the 60-second IFX handler ceiling.

There is no retry logic in the client. Retries are the IFX runtime’s job, and duplicating them here would interact badly with the runtime’s retry budget.


7. The ANS → collector mapping

src/lib/ansToFy.js is the seam between Arc XP’s ANS schema and the collector schema. Two payload shapes are produced:

Upsert (ansToUpsert):

{
action: 'publish',
item_id: ans._id,
site_id: <one of the websites resolved above>,
type: mapAnsTypeToFyType(ans.type),
timestamp: extractTimestamp(ans),
title: extractTitle(ans),
categories: extractCategories(ans),
tags: extractTags(ans),
author: extractAuthor(ans),
is_premium: extractIsPremium(ans),
metadata: { canonical_url: ans.canonical_url || null },
}

Delete (ansToDelete):

{
action: 'delete',
item_id: ans._id,
site_id: <one of the websites resolved above>,
}

Notes on the helpers:

  • mapAnsTypeToFyType collapses story and gallery to article, and passes video through. Unknown types default to article rather than failing.
  • extractTimestamp prefers publish_date, then last_updated_date, then display_date, and finally falls back to “now”. The fallback exists so a malformed ANS document still produces a deterministic payload.
  • extractCategories and extractTags defensively filter(Boolean) so a partially-populated taxonomy never produces null entries.
  • extractIsPremium treats the presence of any subscription as premium. Tenants with a richer subscription model should override this.

This file is the intended customization point. Adding fields here is safe; the handlers do not need to change.


8. Idempotency contract

The bundle’s correctness depends on one invariant:

Two deliveries of the same event must produce byte-identical collector payloads.

The mapping is deterministic on the ANS document — there is no clock read, no random value, no per-invocation state. As long as the ANS document is the same, ansToUpsert(ans, site) returns the same object. The collector treats item_id as an upsert key per site_id, so replays converge instead of duplicating.

test/replayIdempotency.test.js pins this behaviour for all three handlers. That test is run as part of the bundle build, so a regression in the mapping will block a bundle from being produced.


9. Set-up guide

The steps below take you from a clean clone to a working local invocation.

9.1 Prerequisites

  • Node.js 22 or newer.
  • A GitHub PAT with read:packages scope — the Arc XP IFX SDK is published to GitHub Packages and is required to install.
  • A restricted-access Developer PAT issued from your Arc XP Developer Center, to authenticate collector requests.
  • The FY Recommendations service enabled for your organization, with the collector reachable through Arc XP ASI.

9.2 Install

Terminal window
# Tell npm where to find the IFX SDK.
cp .npmrc.example .npmrc
export GITHUB_TOKEN=<your GitHub PAT>
# Install runtime + dev dependencies.
npm install

The postinstall script runs the SDK’s eventsHandlersModuleGenerator, which reads src/eventsRouter.json and produces the glue the IFX SDK expects.

9.3 Configure

Create a local env file from the example and fill in the two collector variables:

Terminal window
cp .env.example .env
$EDITOR .env

Set:

  • FY_COLLECTOR_BASE_URL — the base URL of the collector for your org.
  • FY_COLLECTOR_PAT — your restricted-access Developer PAT.
  • LOG_LEVEL — optional; defaults to info.

.env is gitignored. Do not commit it.

9.4 Run the local testing server

The Arc XP IFX SDK ships a local Express server that lets you invoke handlers without deploying:

Terminal window
npm run localTestingServer

The server boots on a local loopback port and exposes an invoke endpoint that mirrors the runtime’s behaviour. POST a sample ANS event to it and watch the handler dispatch to the collector base URL configured in .env.

9.5 Run the tests

Terminal window
npm test

The suite covers idempotency, payload shape, and input validation. The bundle build invokes the same suite — if it fails, you cannot produce a bundle.


10. Step-by-step: introducing the bundle’s mechanism

If you are walking a new contributor through how an event becomes a collector write, this is the sequence to demonstrate:

  1. Pick an event. Open src/eventsRouter.json and choose one of the wired event names (for example, story:republish).
  2. Find the handler. The router maps that event to storyPublishHandler. Open src/eventsHandlers/storyPublishHandler.js.
  3. Trace validation. Show how event.body._id is enforced and how missing websites throw — both produce IFX retries instead of silent drops.
  4. Trace the mapping. Step into ansToUpsert in src/lib/ansToFy.js. Point out that every helper is pure: same input, same output.
  5. Trace the HTTP call. Step into postContent in src/lib/collectorClient.js. Highlight Arc-Priority: ingestion (loop safety) and the env-driven configuration (no hard-coded URL or credential).
  6. Replay it. Re-send the same event through the local testing server. The collector should see an identical payload — that is the idempotency guarantee in action.
  7. Break it on purpose. Send an event with no _id or no websites. Confirm the handler throws and the runtime would retry. This is the failure mode the bundle is designed around.

After these seven steps a new contributor has touched every file in the bundle and seen the runtime contract end-to-end.


11. Build and ship

When the local flow is working:

Terminal window
# Sandbox bundle, validated against the test suite.
npm run createSandboxBundle
# Production bundle.
npm run createProdBundle

The resulting zip lands in bundles/. Upload it through the Arc XP Developer Center to install the integration on a tenant. The Developer Center is where per-environment values (the collector base URL and the restricted PAT) are injected into the runtime — they are never baked into the bundle itself.

Bundles need to subscribe to the event stream you need to listen; see Subscribing To Events

For more information about bundle management see Managing Your Bundle


12. Extending the bundle

Two common extensions, both safe within the runtime envelope:

  • New content types. Add a handler under src/eventsHandlers/, export it from src/eventsHandlers.js, and add an entry to src/eventsRouter.json. If the new handler also writes to the collector, reuse postContent so loop safety and authentication stay consistent.
  • Richer field mapping. Edit src/lib/ansToFy.js. As long as the helpers remain pure functions of the ANS document, the idempotency contract holds and the existing tests continue to protect you.

When adding either, re-check that handlers stay within the 60-second budget and the bundle stays under the 50 MB code-size ceiling.