Add Kochava (Actions) cloud destination - #3939
Conversation
Adds a new cloud-mode destination for Kochava's Server-to-Server integration with two actions: - event (Post-Install Event): sends in-app events to POST /track/json with action="event" - install (Install Notification): sends install notifications to POST /track/json with action="install" Both actions share a common field set and payload builder (constants.ts / types.ts / utils.ts / fields.ts). Highlights: - Auth: single required App GUID setting (kochava_app_id); Kochava exposes no auth-verification endpoint, so no testAuthentication (matches the Adjust destination pattern). - Device identifiers modeled as idfa/idfv/adid/android_id, assembled into device_ids; at least one is required (PayloadValidationError). - Transforms: device_ua URL-encoded, usertime as epoch seconds, device_limit_tracking as the inverse of context.device.adTrackingEnabled. - No performBatch: /track/json accepts a single record per call (no batch endpoint documented). Tests: 7 unit + 4 snapshot + 3 destination-level (all passing); typecheck and lint clean. TODO before merge: register in destinations/index.ts with the production-assigned metadata ID once the destination is created in Segment production (ID must match across environments). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds a new cloud-mode Kochava destination that forwards Segment Track events to Kochava’s /track/json endpoint, supporting both install notifications and post-install events.
Changes:
- Introduces Kochava destination definition with two actions (
install,event) and shared field definitions. - Adds request-building utilities and types for constructing typed
/track/jsonpayloads (including ATT + AdServices token support). - Adds unit tests, snapshot tests, and destination metadata/preset tests for the new destination.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/destination-actions/src/destinations/kochava/index.ts | Defines the new Kochava cloud destination, presets, and action wiring. |
| packages/destination-actions/src/destinations/kochava/utils.ts | Builds/validates Kochava request bodies and sends requests to /track/json. |
| packages/destination-actions/src/destinations/kochava/types.ts | Adds typed request/response and shared payload shapes for both actions. |
| packages/destination-actions/src/destinations/kochava/fields.ts | Defines shared action fields (device ids, usertime, ATT, etc.). |
| packages/destination-actions/src/destinations/kochava/constants.ts | Adds endpoint constants and action discriminators. |
| packages/destination-actions/src/destinations/kochava/metadata.json | Declares destination + action metadata, fields, and presets. |
| packages/destination-actions/src/destinations/kochava/**/tests/* | Adds unit tests and snapshot tests for the destination and actions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const deviceIds = buildDeviceIds(payload) | ||
| if (Object.keys(deviceIds).length === 0) { | ||
| throw new PayloadValidationError('At least one device identifier (IDFA, IDFV, ADID, or Android ID) is required.') | ||
| } |
| function toEpochSeconds(value?: string | number): number | undefined { | ||
| if (value === undefined || value === null || value === '') return undefined | ||
| const ms = new Date(value).getTime() | ||
| if (Number.isNaN(ms)) return undefined | ||
| return Math.floor(ms / 1000) | ||
| } |
| export interface KochavaDeviceIds { | ||
| idfa?: string | ||
| idfv?: string | ||
| adid?: string | ||
| android_id?: string | ||
| [key: string]: string | undefined | ||
| } |
| device_ua: { | ||
| label: 'Device User Agent', | ||
| description: 'The device user agent string. Either this or Device OS Version is required for OS detection.', | ||
| type: 'string', | ||
| required: false, | ||
| default: { '@path': '$.context.userAgent' } | ||
| }, | ||
| device_ver: { | ||
| label: 'Device OS Version', | ||
| description: 'The device OS version. Either this or Device User Agent is required for OS detection.', | ||
| type: 'string', |
| nock(/.*/).persist().get(/.*/).reply(200) | ||
| nock(/.*/).persist().post(/.*/).reply(200) | ||
| nock(/.*/).persist().put(/.*/).reply(200) |
Adds __tests__/e2e.test.ts covering both actions against a local serve server (./bin/run serve --destination=kochava --noUI): - authentication (valid settings) - event + install delivery via perform() (real HTTP to Kochava) - device-identifier and required-field validation paths - skips for paths not reachable over HTTP (no testAuthentication, no performBatch, live-dependent HTTP error mapping) Uses Node's http module against 127.0.0.1 (Jest's node environment does not expose global fetch, and serve binds IPv4 only). Not run in CI — requires a running serve server. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
packages/destination-actions/src/destinations/kochava/utils.ts:42
- The current non-empty check will accept whitespace-only identifiers (e.g.,
' '), which are effectively empty but will pass validation and be sent downstream. Consider trimming string inputs before the length check (and before assignment) so whitespace-only values are excluded.
for (const [key, val] of candidates) {
if (val !== undefined && val !== null && String(val).length > 0) {
result[key] = String(val)
}
}
packages/destination-actions/src/destinations/kochava/install/tests/index.test.ts:72
- This assertion depends on
undefinedproperties being present in the parsed JSON object. Standard JSON serialization omits keys withundefinedvalues, so this test can become brittle across request-client/serialization changes. Prefer asserting only on the fields that must exist (e.g.,toMatchObject({ att: true, att_time: 1616263616 })) and optionally asserting absent keys withnot.toHaveProperty(...).
expect(body.data.app_tracking_transparency).toEqual({
att: true,
att_time: 1616263616,
att_duration: undefined,
att_detail: undefined
})
packages/destination-actions/src/destinations/kochava/tests/snapshot.test.ts:18
- Using
nock(...).persist()without a matching cleanup (e.g.,nock.cleanAll()/nock.restore()inafterEach/afterAll) can leak interceptors across tests and cause order-dependent failures in the broader test suite. Please add teardown for nock in this file or avoidpersist()if it isn't necessary.
nock(/.*/).persist().get(/.*/).reply(200)
nock(/.*/).persist().post(/.*/).reply(200)
nock(/.*/).persist().put(/.*/).reply(200)
packages/destination-actions/src/destinations/kochava/tests/e2e.test.ts:27
- This e2e suite is documented as out-of-CI but still runs by default when directly targeted, and it can result in real outbound calls to Kochava via the local serve process. To reduce accidental external calls, consider gating the entire suite behind an explicit env flag (e.g.,
if (!process.env.RUN_KOCHAVA_E2E) describe.skip(...)) and/or requiringKOCHAVA_APP_IDto be explicitly set (no dummy default).
const BASE_URL = process.env.BASE_URL ?? 'http://127.0.0.1:3000'
const KOCHAVA_APP_ID = process.env.KOCHAVA_APP_ID ?? 'e2e-dummy-app-guid'
jest.setTimeout(30000)
| function toEpochSeconds(value?: string | number): number | undefined { | ||
| if (value === undefined || value === null || value === '') return undefined | ||
| const ms = new Date(value).getTime() | ||
| if (Number.isNaN(ms)) return undefined | ||
| return Math.floor(ms / 1000) | ||
| } |
Summary
Adds a new cloud-mode destination for Kochava's Server-to-Server integration, forwarding Segment Track events for mobile attribution and analytics.
Two actions, both
POST https://control.kochava.com/track/json, multiplexed by the top-levelactionfield:actionvalueevent)type = "track" and event != "Application Installed"eventinstall)type = "track" and event = "Application Installed"installDesign notes
kochava_app_id(the App GUID). Kochava's S2S API exposes no auth-verification endpoint and uses no token/OAuth, so there is notestAuthentication(matches the existing Adjust destination pattern).idfa/idfv/adid/android_idfields, assembled into Kochava'sdevice_idsobject. Runtime validation requires at least one (PayloadValidationError).device_uaURL-encoded,usertimesent as epoch seconds,device_limit_trackingderived as the inverse ofcontext.device.adTrackingEnabled.att,att_time,att_duration,att_detail) plusad_services_token(install).performBatch:/track/jsonaccepts a single record per call — no batch/bulk endpoint is documented.constants.ts/types.ts/utils.ts/fields.ts; each actionperformis a thin wrapper delegating toutils.sendEvent. All request bodies and responses are typed.Testing
yarn types, typecheck, and lint all cleanRegister this destination in
packages/destination-actions/src/destinations/index.tswith the production-assigned metadata ID once it's created in Segment's production control plane. The ID is a real MongoDB ObjectId that must match across environments (synced via sprout) — intentionally not invented here.Open questions (from spec)
/track/jsonrate limits (undocumented)usertimeis epoch seconds (assumed from sample)device_limit_trackingpolarity (inverse ofadTrackingEnabled)🤖 Generated with Claude Code