Base template to deploy your next source function.
- Click
Use This Templateabove.- (If Segment PS, add to the
Segment Services EngineeringOrganization.)
- (If Segment PS, add to the
nvm use(to get the right version of NodeJS)npm install(to install npm dependencies)
npm run test
- The Buildkite pipeline also runs tests before every deploy.
- Tests live in
src/index.test.js; the function code lives insrc/index.js.
Segment runs src/index.js inside a sandboxed runtime, not in Node. There
is no require, no fs, and no process. Instead the runtime injects a small
set of globals:
| Global | What it is |
|---|---|
request |
The incoming HTTP request. request.json() parses the body. |
settings |
The function's configured settings (e.g. settings.apiKey), managed in the Segment UI on the function's Settings tab. |
Segment |
The event emitter: Segment.track(), .identify(), .group(), .page(), .screen(), .set(). This is how a source function produces data. |
RetryError |
Throw this to ask Segment to retry the invocation. Any other thrown error is a permanent failure. |
fetch, btoa |
Available globally β no import needed. |
Under Jest these globals are stubbed in setup.js, which is what
lets src/index.test.js exercise the handler outside the real runtime.
A minimal handler that calls an upstream API and emits one event:
async function onRequest(request, settings) {
const body = request.json();
let response;
try {
response = await fetch('https://api.example.com/v1/events', {
method: 'POST',
headers: {
Authorization: `Basic ${btoa(settings.apiKey + ':')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
} catch (error) {
// Connection-level failure β worth retrying.
throw new RetryError(error.message);
}
if (response.status >= 500 || response.status === 429) {
// Retry on server errors and rate limits.
throw new RetryError(`Failed with ${response.status}`);
}
// `fetch` resolves to a Response, not to parsed JSON. You must await
// `.json()` before reading fields off the payload.
const payload = await response.json();
Segment.track({
event: 'Example Event',
userId: payload.userId,
properties: { source: 'example-api' }
});
}
β οΈ Gotcha in the shipped placeholder:src/index.jsreadsresponse.propertyNamewithout first awaitingresponse.json(), so that value is alwaysundefined. Parse the body as shown above when you replace the placeholder with your real function.
src/index.js exports onRequest only when NODE_DEV === 'TEST', which
src/index.test.js sets on its first line. The export block is wrapped in
try/catch because the sandboxed runtime has no process at all β an
unguarded process.env reference would throw on deploy.
Deploying a source function is a two-call operation against the Segment
Public API, and it touches three different ids. Getting these straight is
the single most important thing in this repo β mixing them up produces
confusing 400 / "provide a valid Source function instance ID" errors.
| Id | Prefix / shape | What it is | Where you find it |
|---|---|---|---|
| Function id | sfnc_... |
The source-function definition (the code). | Segment UI URL of the function, or GET /functions. |
| Source id | 22-char, e.g. aj3EN6RALrMxFZe7q3Cjd8 |
The connected source that wraps the function. | Source β Settings β API Keys tab ("Source ID"). |
| Source function instance id | sfn_ + the Source id |
The deploy target. | Derived: take the Source id and prefix it with sfn_. |
β οΈ The gotcha: the deploy endpoint (POST /functions/{id}/deploy) does not accept thesfnc_function id, and it does not accept the raw Source id either. It requires thesfn_-prefixed "source function instance" id β i.e. the Source id from the API Keys tab withsfn_prepended. Example: Source idaj3EN6RALrMxFZe7q3Cjd8β deploy idsfn_aj3EN6RALrMxFZe7q3Cjd8.
scripts/deploySourceFunction.js does two API calls per run:
- Push code β
PATCH https://api.segmentapis.com/functions/{FUNCTION_ID}with the contents ofsrc/index.js. Uses thesfnc_id. - Deploy to the connected source β
POST https://api.segmentapis.com/functions/{SOURCE_ID}/deploy. Uses thesfn_id (Source id prefixed withsfn_).
So the two env vars are:
FUNCTION_ID= thesfnc_...function id.SOURCE_ID=sfn_+ the Source id (NOT the raw Source id, NOT thesfnc_id).
(A destination function, by contrast, only PATCHes the function β it has no second "deploy to source" call. That's the core source-vs-destination difference.)
Each environment (DEV / QA / PROD) needs its own function and its own connected source. To create a new environment's pair via the Public API:
POST /functionswith{ "displayName": "<ENV> - Source Function Template", "resourceType": "SOURCE", "code": "<contents of src/index.js>" }. The response includes thesfnc_...id and acatalogId(which is just the id minus thesfnc_prefix).POST /sourceswith{ "slug": "<env>-source-function-template", "enabled": true, "metadataId": "<catalogId from step 1>" }. The response includes the 22-char Source id.- The deploy id for that env is then
sfn_<Source id>.
Tip: the Segment Public API returns the function
codefield with literal unescaped newlines, so piping the create response throughjqfails. Extract ids withgrep -oE '"id":"sfnc_[A-Za-z0-9]+"'instead β the resource is still created successfully.
All three environments live in the same Segment workspace; naming them
DEV - / QA - / PROD - Source Function Template keeps them
distinguishable.
This is the out-of-the-box deploy path β if you used this template, this is
what works with no extra infrastructure. The workflow is defined in
.github/workflows/deploySourceFunction.yml;
it installs dependencies, runs tests, then deploys via scripts/deploySourceFunction.js.
- Create GitHub Environments in
SettingsβEnvironmentsβDEV(repeat forQA&PROD).DEVis enabled by default in the workflow; uncomment theQA/PRODjobs when you're ready to promote to them. - Create the function + connected source per environment in your Segment workspace (see "One function + one source per environment" above).
- Create a Public API Token to allow deploying.
- Add these Environment Secrets to each environment:
FUNCTION_IDβ thesfnc_...function id.SOURCE_IDβ thesfn_-prefixed source function instance id (the Source id from Settings β API Keys, withsfn_prepended).PUBLIC_API_TOKENβ get an API token.
To promote: add the !!_RELEASE_TO_QA label to the PR to deploy to QA; merge to
main to deploy to PROD.
Inside Twilio we deploy through Buildkite instead of GitHub-hosted runners. The
pipeline is defined in .buildkite/pipeline.yml;
each environment step installs dependencies, runs tests, then deploys via the
same scripts/deploySourceFunction.js.
- Create a Buildkite pipeline pointed at this repo (GitHub webhook + a queue
with connected agents). On the
twilio-primary-defaultcluster, use a live general-purpose queue such asgeneral-001β thedefaultqueue is paused and has no agents, so jobs sent there hang in "queuing". - Store the deploy secrets in AWS Secrets Manager (region
us-west-2, account058449100246/ success-write) and let the pipeline read them at deploy time by assuming an IAM role via the Buildkite OIDC provider β no static keys, no Buildkite-stored secrets:segment/source-function-template/public-api-tokenβ{ "PUBLIC_API_TOKEN": "..." }(shared across all envs).segment/source-function-template/<dev|qa|prod>/function-idβ{ "FUNCTION_ID": "sfnc_...", "SOURCE_ID": "sfn_..." }(per env; holds BOTH ids the two-call deploy needs).- IAM role
twilio-internal_source-function-templateis assumed by theaws-assume-role-with-web-identityplugin; its trust policy is scoped to this pipeline's slug and it may only read these secrets. (Created manually in success-write, mirroringdestination-function-templateβ not in Terraform.) - The deploy step sources
.buildkite/fetch-secrets.shto loadPUBLIC_API_TOKEN,FUNCTION_ID, andSOURCE_IDfor the targetDEPLOY_ENV.
- Promote the same way:
!!_RELEASE_TO_QAlabel β QA step; merge tomainβ PROD step. Per-step commit statuses (buildkite/<slug>/dev,/qa,/prod) gate the PR merge.
- Jest for code testing (Istanbul/
babelcoverage provider for honest branch coverage) - Prettier for code formatting
- ESLint (flat config) for code linting
- Buildkite pipeline for function deploy
- Husky for commit validation
