diff --git a/.claude/skills/api-version-upgrade/SKILL.md b/.claude/skills/api-version-upgrade/SKILL.md new file mode 100644 index 00000000000..614a53b27e3 --- /dev/null +++ b/.claude/skills/api-version-upgrade/SKILL.md @@ -0,0 +1,1420 @@ +--- +name: api-version-upgrade +description: Upgrade API versions for Segment Action Destinations with feature flags, comprehensive breaking change analysis, automated testing, and PR creation. +version: 1.0.0 +allowed-tools: + - Read + - Write + - Edit + - Glob + - Grep + - WebFetch + - AskUserQuestion + - Bash +disable-model-invocation: false +--- + +# API Version Upgrade Workflow + +This skill automates the complete process of upgrading API versions for Segment Action Destinations, ensuring safe rollouts with feature flags and comprehensive testing. + +## Console Output Standard + +**MANDATORY**: At the start of every step, print the step header exactly as shown below. At the end of every step, print the completion line. Do not summarize inline — use the structured format consistently. + +### Step header format + +Steps are numbered 0–8 with one sub-step (5.5), giving 10 total headers. Use the exact step label shown in each section (e.g. `0/8`, `5.5/8`). + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[STEP N/8] +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### Step completion format + +``` +✅ Step N complete: +``` + +### Inline status items + +For individual checks or actions within a step, use: + +``` + • : +``` + +### Error format + +``` +❌ Step N failed: + → +``` + +This format must be used verbatim every run so output is easy to scan and consistent across destinations. + +## Overview + +API version upgrades in action-destinations follow a **canary pattern**: + +- Stable production version remains unchanged +- New version is deployed behind a feature flag +- Gradual rollout allows safe testing and rollback +- Full breaking changes analysis before merge + +## ⚠️ MANDATORY REQUIREMENTS ⚠️ + +**EVERY API version upgrade MUST include:** + +1. **versioning-info.ts file** - If it doesn't exist, CREATE IT. No exceptions. (Both cloud and browser modes) +2. **Version control mechanism** - Depends on mode: + - **Cloud mode**: Runtime feature flag with `getApiVersion(features)` helper + - **Browser mode**: Opt-in via settings dropdown, no runtime feature flag +3. **Version tests** - Test both stable (default) and new version: + - **Cloud mode**: Feature flag tests with `features` parameter + - **Browser mode**: SDK loading tests with explicit version selection + +These are not optional. They are required for safe, gradual rollouts and instant rollback capability. + +## Step 0: Pre-flight Tool Check + +Print at start: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[STEP 0/8] PRE-FLIGHT TOOL CHECK +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +Before doing anything else, verify all required tools are installed. Run each check and report the results in a summary table. + +```bash +which git && git --version +which gh && gh --version +which yarn && yarn --version +which node && node --version +which nvm || (source ~/.nvm/nvm.sh && nvm --version) +``` + +| Tool | Required | Purpose | +| ------ | -------- | ---------------------------------- | +| `git` | REQUIRED | Branch management, commits | +| `gh` | REQUIRED | Pull request creation | +| `yarn` | REQUIRED | Running tests | +| `node` | REQUIRED | Running tests (v18.17+ or v22.13+) | +| `nvm` | OPTIONAL | Switching Node versions | + +**If any REQUIRED tool is missing, stop immediately and tell the user:** + +``` +❌ Pre-flight check failed. + +Missing required tools: +- : + +Please install the missing tools and re-run the skill. +``` + +Install hints: + +- `gh` not found: `brew install gh` (Mac) or see https://cli.github.com +- `yarn` not found: `npm install -g yarn` +- `git` not found: `brew install git` or install Xcode Command Line Tools +- `node` not found: install via https://nodejs.org or `nvm install 22` +- `nvm` not found (optional): `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash` + +If `nvm` is missing but node is already at v18.17+ or v22.13+, that is acceptable — skip the `nvm use` step later. + +Only continue to the next step after all REQUIRED tools are confirmed present. + +Print after checks complete: + +``` + • git: + • gh: + • yarn: + • node: + • nvm: +✅ Step 0 complete: all required tools present +``` + +Or on failure: + +``` +❌ Step 0 failed: missing required tools: + → Install missing tools and re-run +``` + +## Prerequisites Check + +Before starting, verify: + +1. You're in the action-destinations repository root +2. Node version is compatible (v18.17+ or v22.13+) +3. Git working directory is clean +4. User has provided: + - Destination name (e.g., "klaviyo", "google-campaign-manager-360") + - Target API version (e.g., "2026-01-15", "v5") + +If any information is missing, ask the user before proceeding. + +## Step 1: Information Gathering + +Print at start: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[STEP 1/8] INFORMATION GATHERING +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### 1.1 Collect Required Information + +Ask the user for: + +``` +- Destination name: [e.g., klaviyo] +- Current version: [will auto-detect if not provided] +- Target version: [user must provide] +- Changelog URL (optional): [base URL for API docs] +``` + +### 1.2 Detect Destination Mode + +Determine if this is a cloud-mode or browser-mode destination: + +```bash +# Check for cloud-mode destination +if [ -d "packages/destination-actions/src/destinations/" ]; then + echo "CLOUD" + DEST_PATH="packages/destination-actions/src/destinations/" +# Check for browser-mode destination +elif [ -d "packages/browser-destinations/destinations/" ]; then + echo "BROWSER" + DEST_PATH="packages/browser-destinations/destinations//src" +else + echo "NOT FOUND" +fi +``` + +**Critical difference:** + +- **CLOUD mode**: Version used internally in API calls, controlled by runtime feature flag +- **BROWSER mode**: Customer selects SDK version from dropdown, loaded from external CDN + +If both paths exist or neither exists, ask the user to clarify. + +### 1.3 Locate Destination Files + +Key files to check (paths vary by mode): + +**Cloud Mode** (`packages/destination-actions/src/destinations//`): + +- `versioning-info.ts` - version constants (CREATE if missing) +- `config.ts` - may have version constants +- `functions.ts` or `utils.ts` - API request building with version +- `index.ts` - main destination definition +- `__tests__/` - test files (action-specific) + +**Browser Mode** (`packages/browser-destinations/destinations//src/`): + +- `versioning-info.ts` - version constants (CREATE if missing) +- `index.ts` - destination with `settings.sdkVersion.choices` array +- `__tests__/initialization.test.ts` - SDK loading tests + +### 1.4 Detect Current Version + +Search for version constants in this order: + +1. `versioning-info.ts` (preferred pattern) +2. `config.ts` (simple pattern) +3. Hardcoded in `utils.ts` or `functions.ts` + +Common patterns: + +```typescript +// Pattern 1: versioning-info.ts (PREFERRED - Google CM360 style) +export const DESTINATION_API_VERSION = 'v4' +export const DESTINATION_CANARY_API_VERSION = 'v5' + +// Pattern 2: config.ts (Klaviyo style) - MIGRATE TO versioning-info.ts +export const REVISION_DATE = '2025-01-15' +export const API_URL = 'https://api.example.com' + +// Pattern 3: Inline constant - MIGRATE TO versioning-info.ts +const API_VERSION = 'v3' +const BASE_URL = `https://api.example.com/${API_VERSION}` +``` + +**If versioning-info.ts does NOT exist, you MUST create it in Step 4A or 4B (depending on mode).** + +Print after step completes: + +``` + • Destination: + • Mode: CLOUD | BROWSER + • Directory: + • Current version: + • Target version: + • versioning-info.ts: + + → Next: Proceed to Step 2 (Changelog Analysis) + → Then: Step 4A (Cloud Mode) | Step 4B (Browser Mode) +✅ Step 1 complete: destination located, versions identified, mode detected +``` + +## Step 2: Changelog Analysis + +Print at start: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[STEP 2/8] CHANGELOG ANALYSIS +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### 2.1 Determine Changelog Location + +Check if `versioning-info.ts` has a changelog URL comment: + +```typescript +/** DESTINATION_API_VERSION + * API version documentation. + * Changelog: https://developers.example.com/changelog + */ +``` + +If not present, ask user for changelog URL or look for common patterns: + +- `https://developers.{destination}.com/changelog` +- `https://developers.{destination}.com/docs/api-versions` +- `https://{destination}.com/api/reference` + +### 2.2 Fetch Changelog + +Use WebFetch to retrieve changelog for the version range: + +```bash +# Fetch the main changelog page + + +# Look for version-specific pages if needed + +``` + +### 2.3 Deep Breaking Changes Analysis + +**CRITICAL: This analysis must be thorough. We cannot afford discrepancies.** + +For each API change between current and target version, check: + +#### Request Changes + +- [ ] New required parameters +- [ ] Removed or deprecated parameters +- [ ] Changed parameter types or formats +- [ ] Modified validation rules +- [ ] Different authentication methods +- [ ] New headers required +- [ ] Changed request body structure + +#### Response Changes + +- [ ] Modified response schema +- [ ] Removed response fields +- [ ] Changed field types +- [ ] Different error codes +- [ ] New error response formats +- [ ] Pagination changes + +#### Behavioral Changes + +- [ ] Rate limiting differences +- [ ] Batching size limits +- [ ] Timeout changes +- [ ] Retry logic requirements +- [ ] Idempotency key handling + +#### Endpoint Changes + +- [ ] URL pattern changes +- [ ] Method changes (GET → POST, etc.) +- [ ] Deprecated endpoints +- [ ] New endpoints replacing old ones + +Create a structured breaking changes document: + +```markdown +## Breaking Changes Analysis: {Current Version} → {Target Version} + +### Summary + +[High-level overview of changes] + +### Critical Breaking Changes + +1. **[Category]**: [Description] + - **Impact**: [How this affects our implementation] + - **Required Action**: [What needs to be changed] + - **Risk Level**: HIGH/MEDIUM/LOW + +### Non-Breaking Changes + +- [List of additive or compatible changes] + +### Deprecation Warnings + +- [Features deprecated but still functional] + +### Testing Requirements + +- [Specific scenarios that must be tested] +``` + +Save this analysis to `breaking-changes-analysis.md` in the destination directory. + +Print after step completes: + +``` + • Changelog URL: + • Pages fetched: + • Critical breaking changes: + • Medium priority changes: + • Low priority changes: + • Analysis saved to: breaking-changes-analysis.md +✅ Step 2 complete: breaking changes analysis written +``` + +## Step 3: Git Branch Setup + +Print at start: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[STEP 3/8] GIT BRANCH SETUP +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### 3.1 Pull Latest Main + +```bash +git fetch origin +git checkout main +git pull origin main +``` + +### 3.2 Create Feature Branch + +Use naming convention: `{destination}-api-{target-version}` + +```bash +git checkout -b klaviyo-api-2026-01-15 +# or +git checkout -b google-cm360-api-v5 +``` + +Print after step completes: + +``` + • Base branch: main () + • New branch: +✅ Step 3 complete: on branch , up to date with main +``` + +## Step 4: Implement Version Upgrade + +Print at start: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[STEP 4/8] IMPLEMENT VERSION UPGRADE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### Routing: Choose Implementation Path + +Based on the destination mode detected in Step 1: + +- **CLOUD mode** → Proceed to **Step 4A** (Feature Flag Implementation) +- **BROWSER mode** → Proceed to **Step 4B** (Opt-In Version Selection) + +--- + +## Step 4A: Cloud Mode - Feature Flag Implementation + +**Use this section ONLY for CLOUD-MODE destinations.** + +### ⚠️ CRITICAL: Feature Flag is MANDATORY for Cloud Destinations ⚠️ + +**ALL cloud-mode API version upgrades MUST be behind a feature flag.** +**If versioning-info.ts does NOT exist, you MUST create it.** + +There is NO option to upgrade the version directly without a feature flag. + +### 4.1 Check if versioning-info.ts Exists + +```bash +ls packages/destination-actions/src/destinations/{destination}/versioning-info.ts +``` + +**Two possible scenarios:** + +--- + +### 4.2 Scenario A: versioning-info.ts EXISTS + +If `versioning-info.ts` already exists, update it: + +**Step 1**: Update `versioning-info.ts` with new canary version: + +```typescript +/** DESTINATION_API_VERSION + * {Destination} API version (stable/production). + * Changelog: {URL} + */ +export const DESTINATION_API_VERSION = '{current-version}' + +/** DESTINATION_CANARY_API_VERSION + * {Destination} API version (canary/feature-flagged). + * Testing new version {target-version} behind feature flag. + */ +export const DESTINATION_CANARY_API_VERSION = '{target-version}' +``` + +**Step 2**: Verify or create helper in `utils.ts` or `functions.ts`: + +```typescript +import { Features } from '@segment/actions-core' +import { DESTINATION_API_VERSION, DESTINATION_CANARY_API_VERSION } from './versioning-info' + +export const API_VERSION = DESTINATION_API_VERSION +export const CANARY_API_VERSION = DESTINATION_CANARY_API_VERSION +export const FLAGON_NAME = '{destination-slug}-canary-version' + +export function getApiVersion(features?: Features): string { + return features && features[FLAGON_NAME] ? CANARY_API_VERSION : API_VERSION +} +``` + +**Step 3**: Update all API calls to use `getApiVersion(features)`: + +```typescript +// Before +const response = await request(`${API_URL}/${API_VERSION}/endpoint`) + +// After +const version = getApiVersion(features) +const response = await request(`${API_URL}/${version}/endpoint`) +``` + +**Step 4**: Ensure `features` parameter is passed through actions: + +```typescript +// In action's perform or performBatch +async perform(request, { payload, settings, features }) { + const version = getApiVersion(features) + // use version in API calls +} + +async performBatch(request, { payload, settings, features }) { + const version = getApiVersion(features) + // use version in API calls +} +``` + +--- + +### 4.3 Scenario B: versioning-info.ts DOES NOT EXIST + +**If versioning-info.ts doesn't exist, you MUST create it. This is mandatory.** + +**Step 1**: Create `versioning-info.ts` file: + +```typescript +/** DESTINATION_API_VERSION + * {Destination} API version (stable/production). + * Changelog: {changelog-url} + */ +export const DESTINATION_API_VERSION = '{current-version}' + +/** DESTINATION_CANARY_API_VERSION + * {Destination} API version (canary/feature-flagged). + * Testing new version {target-version} behind feature flag. + */ +export const DESTINATION_CANARY_API_VERSION = '{target-version}' +``` + +**Step 2**: Find where the current version is defined: + +Search for version constants: + +```bash +grep -r "REVISION_DATE\|API_VERSION\|VERSION" --include="*.ts" packages/destination-actions/src/destinations/{destination}/ | grep -v test | grep -v node_modules +``` + +Common locations: + +- `config.ts`: `export const REVISION_DATE = '2025-01-15'` +- `utils.ts`: `const API_VERSION = 'v4'` +- Inline in API calls: `https://api.example.com/v4/endpoint` + +**Step 3**: Update the file with the old version constant: + +If in `config.ts`: + +```typescript +// Before +export const REVISION_DATE = '2025-01-15' + +// After - import from versioning-info +import { DESTINATION_API_VERSION } from './versioning-info' +export const REVISION_DATE = DESTINATION_API_VERSION +``` + +If in `utils.ts`: + +```typescript +// Before +const API_VERSION = 'v4' + +// After - import from versioning-info +import { DESTINATION_API_VERSION } from './versioning-info' +export const API_VERSION = DESTINATION_API_VERSION +``` + +**Step 4**: Add feature flag helper to `utils.ts` or `functions.ts`: + +> ⚠️ **Import order**: Keep all `import` statements grouped at the top. Add `FLAGON_NAME` and `getApiVersion` after the last import rather than between imports — this matches the repository's usual style and avoids confusing the reader. + +```typescript +// All imports first +import { Features } from '@segment/actions-core' +import { DESTINATION_API_VERSION, DESTINATION_CANARY_API_VERSION } from './versioning-info' +// ... all other imports ... + +// Exports after all imports +export const API_VERSION = DESTINATION_API_VERSION +export const CANARY_API_VERSION = DESTINATION_CANARY_API_VERSION +export const FLAGON_NAME = '{destination-slug}-canary-version' + +export function getApiVersion(features?: Features): string { + return features && features[FLAGON_NAME] ? CANARY_API_VERSION : API_VERSION +} +``` + +**Step 5**: Update all places where version is used: + +Find all usage sites: + +```bash +grep -r "{old-version-constant}" --include="*.ts" packages/destination-actions/src/destinations/{destination}/ +``` + +Update them to use `getApiVersion(features)`: + +```typescript +// Example 1: Direct API call +// Before +const response = await request(`https://api.example.com/v4/endpoint`) + +// After +const version = getApiVersion(features) +const response = await request(`https://api.example.com/${version}/endpoint`) + +// Example 2: In a helper function +// Before +export function buildUrl(path: string) { + return `https://api.example.com/v4/${path}` +} + +// After +export function buildUrl(path: string, features?: Features) { + const version = getApiVersion(features) + return `https://api.example.com/${version}/${path}` +} +``` + +**Step 6**: Update `extendRequest` in `index.ts` if needed: + +If the version is used in headers or base URL: + +```typescript +// Add features parameter +extendRequest({ settings, features }) { + const version = getApiVersion(features) + return { + headers: { + 'api-version': version, + // ... other headers + } + } +} +``` + +**Step 7**: Update action perform functions to pass `features`: + +```typescript +// In each action file (e.g., conversionUpload/index.ts) +async perform(request, { payload, settings, features }) { + // Make sure to pass features to any helper functions + return await sendRequest(request, settings, payload, features) +} + +async performBatch(request, { payload, settings, features }) { + return await sendRequest(request, settings, payload, features) +} +``` + +--- + +### 4.4 Verify Feature Flag Implementation + +**MANDATORY CHECKLIST** - verify ALL items before proceeding: + +```markdown +- [ ] versioning-info.ts file EXISTS with both DESTINATION_API_VERSION and DESTINATION_CANARY_API_VERSION +- [ ] FLAGON_NAME constant defined with format '{destination-slug}-canary-version' +- [ ] FLAGON_NAME and getApiVersion are placed AFTER all import statements (import/first lint rule) +- [ ] getApiVersion(features) helper function implemented in utils.ts or functions.ts +- [ ] All API calls use getApiVersion(features) instead of hardcoded version +- [ ] extendRequest passes features parameter if version is used there +- [ ] All action perform/performBatch functions accept features parameter +- [ ] Features parameter is passed to all helper functions that need it +- [ ] No hardcoded version strings remain in the codebase +- [ ] Tests can toggle feature flag +``` + +**If ANY item is not checked, the implementation is INCOMPLETE.** + +Print after step completes: + +``` + • versioning-info.ts: + • Scenario: + • FLAGON_NAME: + • getApiVersion helper: + • API call sites updated: + • Actions updated: +✅ Step 4A complete: feature flag implemented, all API calls use getApiVersion() +``` + +--- + +## Step 4B: Browser Mode - Opt-In Version Selection + +**Use this section ONLY for BROWSER-MODE destinations.** + +### Architecture Note + +Browser destinations work fundamentally differently from cloud destinations: + +- **Version Selection**: Customers explicitly choose SDK version from a dropdown in settings +- **SDK Loading**: Version determines which SDK is loaded from external CDN (e.g., `https://js.appboycdn.com/web-sdk/{version}/...`) +- **No Runtime Feature Flag**: Browser destinations don't receive `features` parameter in actions +- **"Canary" Pattern**: Means "newly available option for customers to opt-in to" +- **Safe Rollout**: Keep default at stable version, add new version to choices array + +### 4B.1 Check if versioning-info.ts Exists + +```bash +ls packages/browser-destinations/destinations/{destination}/src/versioning-info.ts +``` + +### 4B.2 Create or Update versioning-info.ts + +Create the file with version constants: + +```typescript +/** DESTINATION_API_VERSION + * {Destination} SDK version (stable/default for new installations). + * This is the default version selected for new destination configurations. + * Changelog: {URL} + */ +export const DESTINATION_API_VERSION = '{current-version}' + +/** DESTINATION_CANARY_API_VERSION + * {Destination} SDK version (newly available option). + * Version {target-version} is available for customers to explicitly select in settings. + * This allows customers to opt-in to the latest version before it becomes the default. + */ +export const DESTINATION_CANARY_API_VERSION = '{target-version}' +``` + +### 4B.3 Update index.ts + +**Step 1**: Import version constant and re-export for tests: + +```typescript +// At top of file +import { DESTINATION_API_VERSION } from './versioning-info' + +// Re-export for tests (after imports, before other code) +export { DESTINATION_API_VERSION, DESTINATION_CANARY_API_VERSION } from './versioning-info' +``` + +**Step 2**: Update defaultVersion to use constant: + +```typescript +// Before +const defaultVersion = '6.1' + +// After +// Default version for new installations (stable) +// Customers can explicitly select DESTINATION_CANARY_API_VERSION from settings +const defaultVersion = DESTINATION_API_VERSION +``` + +**Step 3**: Add new version to settings choices: + +Find the `settings` object with the version dropdown (usually `sdkVersion` or similar field): + +```typescript +settings: { + sdkVersion: { + label: 'SDK Version', + type: 'string', + choices: [ + // ... existing versions ... + { + value: '{current-version}', + label: '{current-version}' + }, + // ADD THIS: + { + value: '{target-version}', + label: '{target-version}' + } + ], + default: defaultVersion, + required: true + }, + // ... other settings ... +} +``` + +### 4B.4 Verification Checklist + +**MANDATORY CHECKLIST** - verify ALL items: + +```markdown +- [ ] versioning-info.ts file EXISTS with both DESTINATION_API_VERSION and DESTINATION_CANARY_API_VERSION +- [ ] DESTINATION_API_VERSION set to current stable version +- [ ] DESTINATION_CANARY_API_VERSION set to new target version +- [ ] index.ts imports DESTINATION_API_VERSION +- [ ] index.ts re-exports both constants for tests +- [ ] defaultVersion uses DESTINATION_API_VERSION (not hardcoded) +- [ ] New version added to settings choices array +- [ ] Default version remains stable (not changed to canary) +- [ ] No getApiVersion() function (not needed for browser mode) +- [ ] No FLAGON_NAME constant (not needed for browser mode) +- [ ] No Features import or usage (browser actions don't receive features) +``` + +**If ANY item is not checked, the implementation is INCOMPLETE.** + +Print after step completes: + +``` + • versioning-info.ts: + • DESTINATION_API_VERSION: + • DESTINATION_CANARY_API_VERSION: + • Settings choices updated: added {target-version} + • Default version: remains {stable-version} (safe rollout) + • Pattern: Opt-in selection (no runtime feature flag) +✅ Step 4B complete: new version available for customer selection +``` + +--- + +## Step 5: Update Tests + +Print at start: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[STEP 5/8] UPDATE TESTS +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### 5.1 Auto-Detect Test Pattern + +Test paths vary by mode: + +**Cloud Mode:** + +``` +packages/destination-actions/src/destinations/{destination}/__tests__/ +``` + +**Browser Mode:** + +``` +packages/browser-destinations/destinations/{destination}/src/__tests__/ +``` + +Find test files: + +```bash +# Cloud mode +find packages/destination-actions/src/destinations/{destination} -name "*.test.ts" -o -name "*.test.js" + +# Browser mode +find packages/browser-destinations/destinations/{destination}/src -name "*.test.ts" -o -name "*.test.js" +``` + +### 5.2 Update Existing Tests + +Update existing tests to use `API_VERSION` constant instead of hardcoded version strings: + +```typescript +// Before +import Destination from '../../index' + +nock('https://api.example.com/v4/endpoint').post('').reply(200, {}) + +// After +import Destination from '../../index' +import { API_VERSION } from '../../utils' + +nock(`https://api.example.com/${API_VERSION}/endpoint`).post('').reply(200, {}) +``` + +### 5.3 Add Version Tests (MANDATORY) + +Choose the appropriate test pattern based on destination mode: + +#### For Cloud Mode: Feature Flag Tests + +**Add a new test suite** for feature flag behavior: + +```typescript +describe('API Version Feature Flag', () => { + it('should use stable API version by default', async () => { + const event = createTestEvent({ + // ... event data + }) + + nock(`https://api.example.com/${API_VERSION}/endpoint`).post('').reply(200, {}) + + const responses = await testDestination.testAction('actionName', { + event, + mapping: { + // ... required mappings + }, + useDefaultMappings: true, + settings + // NO features parameter = uses stable version + }) + + expect(responses[0].status).toBe(200) + }) + + it('should use canary API version when feature flag is enabled', async () => { + const event = createTestEvent({ + // ... same event data + }) + + // Should call canary version endpoint + nock(`https://api.example.com/${CANARY_API_VERSION}/endpoint`).post('').reply(200, {}) + + const responses = await testDestination.testAction('actionName', { + event, + mapping: { + // ... same mappings + }, + useDefaultMappings: true, + settings, + features: { [FLAGON_NAME]: true } // Feature flag enabled + }) + + expect(responses[0].status).toBe(200) + }) +}) +``` + +**IMPORTANT**: Add these tests for EVERY action in the destination (conversionUpload, identify, track, etc.) + +#### For Browser Mode: SDK Version Loading Tests + +**Add tests to `__tests__/initialization.test.ts`**: + +```typescript +import { DESTINATION_API_VERSION, DESTINATION_CANARY_API_VERSION } from '../versioning-info' + +describe('SDK Version Tests', () => { + test('uses stable SDK version by default', async () => { + const [event] = await brazeDestination({ + api_key: 'test_key', + endpoint: 'sdk.example.com', + // sdkVersion not specified - should use default + subscriptions: [ + /* ... */ + ] + }) + + await event.load(Context.system(), {} as Analytics) + + const scripts = window.document.querySelectorAll('script') + const sdkScript = Array.from(scripts).find((script) => script.src.includes('cdn.example.com')) + + expect(sdkScript?.src).toBe(`https://cdn.example.com/sdk/${DESTINATION_API_VERSION}/sdk.min.js`) + }) + + test('can load canary SDK version when explicitly selected', async () => { + const [event] = await brazeDestination({ + api_key: 'test_key', + endpoint: 'sdk.example.com', + sdkVersion: DESTINATION_CANARY_API_VERSION, // Explicitly select new version + subscriptions: [ + /* ... */ + ] + }) + + await event.load(Context.system(), {} as Analytics) + + const scripts = window.document.querySelectorAll('script') + const sdkScript = Array.from(scripts).find((script) => script.src.includes('cdn.example.com')) + + expect(sdkScript?.src).toBe(`https://cdn.example.com/sdk/${DESTINATION_CANARY_API_VERSION}/sdk.min.js`) + }) + + test('verifies new SDK version is available in settings choices', () => { + const sdkVersionField = destination.settings.sdkVersion + const choices = sdkVersionField?.choices || [] + const hasNewVersion = choices.some((choice) => choice.value === DESTINATION_CANARY_API_VERSION) + + expect(hasNewVersion).toBe(true) + expect(sdkVersionField?.default).toBe(DESTINATION_API_VERSION) + }) +}) +``` + +**Key differences for browser mode:** + +- No `features` parameter +- Test SDK loading from CDN (check script src) +- Verify version in settings choices array +- Usually in initialization.test.ts (not action-specific tests) + +### 5.4 Run Tests + +Switch to compatible Node version and run tests based on mode: + +**Cloud Mode:** + +```bash +# Switch Node version if needed +source ~/.nvm/nvm.sh && nvm use 22.13.1 + +# Run destination-specific tests +TZ=UTC yarn cloud test --testPathPattern=src/destinations/{destination} --no-coverage +``` + +**Browser Mode:** + +```bash +# Switch Node version if needed +source ~/.nvm/nvm.sh && nvm use 22.13.1 + +# Run browser destination tests +TZ=UTC yarn test --testPathPattern="destinations/{destination}" --no-coverage +``` + +**Expected outcome**: All tests must pass. If tests fail: + +1. Review breaking changes analysis +2. Update implementation to handle differences +3. Re-run tests until all pass + +### 5.5 Handle Test Failures + +If tests fail due to breaking changes: + +1. **Identify failure cause** from test output +2. **Check breaking changes analysis** for related changes +3. **Update implementation**: + + - Modify request/response handling + - Add new required fields + - Update validation logic + - Adjust error handling + +4. **Add regression tests** for the specific breaking change +5. **Re-run tests** + +Repeat until all tests pass. + +Print after step completes: + +``` + • Test files found: + • Existing tests updated: + • Feature flag tests added: (stable + canary per action) + • Test suites: / + • Tests: / + • Duration: +✅ Step 5 complete: all tests passing +``` + +Or on failure: + +``` +❌ Step 5 failed: tests failing + → +``` + +## Step 5.5: Run API Validation Against Real Endpoints + +Print at start: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[STEP 5.5/8] API VALIDATION AGAINST REAL ENDPOINTS +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +**Note**: This step applies primarily to **CLOUD-MODE** destinations where we make server-to-server API calls. For **BROWSER-MODE** destinations, skip this step as the SDK is loaded from an external CDN and validation would require browser automation. + +This step makes real HTTP calls to both the stable and canary revisions and structurally diffs the responses. It catches breaking changes that mocked unit tests cannot detect. + +### Prerequisites + +- `KLAVIYO_TEST_API_KEY` (or equivalent) set in env — use a test account, never production +- A pre-existing test list ID for the destination + +### Run the validation script + +This step is **optional** and requires you to first create a validation script at a path of your choosing (e.g. `packages/destination-actions/src/destinations/{destination}/validate.ts`). There is no pre-existing `__validation__` directory in this repo — you must create the script if you want live endpoint validation. + +```bash +DESTINATION_TEST_API_KEY=xxx \ +npx ts-node packages/destination-actions/src/destinations/{destination}/validate.ts +``` + +When chamber is available: + +```bash +chamber exec {destination}-test -- npx ts-node .../validate.ts +``` + +### What it does + +- Fires each fixture against **both** revisions sequentially (stable first, then canary) +- Each write fixture uses revision-scoped identifiers so calls never conflict +- Normalizes non-deterministic fields (IDs, timestamps) before diffing +- Exits non-zero if any structural differences are found + +### Expected outcome + +``` +✅ All N endpoints are structurally identical across both revisions. Safe to promote canary. +``` + +If differences are found, review the script output for the specific fields that changed and update the implementation accordingly before proceeding. + +### Notes + +- If you produce a validation report, include a **sanitized summary** in the PR description rather than committing raw output (it may contain PII or sensitive API responses) +- The script should use a timestamp-based `RUN_ID` so repeated runs never collide on the same test profiles/events + +Print after step completes: + +``` + • Endpoints validated: + • Structural differences: + • Validation report: __validation__/validation-report.md +✅ Step 5.5 complete: +``` + +## Step 6: Commit Changes + +Print at start: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[STEP 6/8] COMMIT CHANGES +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### 6.1 Review Changes + +```bash +git status +git diff +``` + +Verify: + +- versioning-info.ts created or updated correctly +- Feature flag implemented properly +- All usages updated to use getApiVersion() +- Tests updated and passing +- No unintended changes + +### 6.2 Stage and Commit + +```bash +# Stage all changes +git add packages/destination-actions/src/destinations/{destination}/ + +# Commit with descriptive message +git commit -m "feat({destination}): upgrade API to {target-version} behind feature flag + +- Add/update versioning-info.ts with canary version {target-version} +- Implement feature flag '{flagon-name}' +- Update API calls to use getApiVersion() helper +- Add tests for both stable and canary versions +- All tests passing + +Breaking changes analysis in breaking-changes-analysis.md + +Co-Authored-By: Claude Sonnet 4.5 " +``` + +Print after step completes: + +``` + • Files staged: + • Commit SHA: +✅ Step 6 complete: changes committed on +``` + +## Step 7: Push and Create PR + +Print at start: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[STEP 7/8] PUSH AND CREATE PR +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### 7.1 Push Branch + +```bash +git push origin {branch-name} -u +``` + +### 7.2 Create Pull Request + +Use GitHub CLI to create PR with comprehensive description: + +```bash +gh pr create --title "feat({destination}): Upgrade API to {target-version} with feature flag" --body "$(cat <<'EOF' +## Summary + +Upgrades {Destination} API from **{current-version}** to **{target-version}**, deployed behind feature flag `{flagon-name}`. + +## Changes + +### Version Management +- ✅ Created/updated `versioning-info.ts` with canary pattern +- ✅ Implemented `getApiVersion(features)` helper function +- ✅ Updated all API calls to use feature-flagged version +- ✅ Added feature flag constant: `{flagon-name}` + +### Testing +- ✅ All existing tests pass +- ✅ Added tests for both stable and canary versions +- ✅ Test pattern: `destinations/{destination}` +- ✅ Test results: **{X} test suites passed, {Y} tests passed** + +## Breaking Changes + +{Insert breaking changes analysis here - read from breaking-changes-analysis.md} + +### Critical Breaking Changes +{List high-priority breaking changes} + +### Medium Priority Changes +{List medium-priority changes} + +### Low Priority / Informational +{List low-priority changes} + +## Testing Plan + +### Manual Testing Required +- [ ] Test with feature flag disabled (stable version) +- [ ] Test with feature flag enabled (canary version) +- [ ] Verify no regression in existing functionality +- [ ] Test edge cases identified in breaking changes + +### Automated Testing +- [x] Unit tests passing +- [x] Integration tests passing (if applicable) +- [x] Snapshot tests updated (if applicable) + +## Rollout Plan + +1. **Phase 1**: Merge PR, feature flag off by default +2. **Phase 2**: Enable for internal testing +3. **Phase 3**: Gradual rollout to subset of customers +4. **Phase 4**: Full rollout, promote canary to stable +5. **Phase 5**: Remove feature flag, clean up old version + +## Risk Assessment + +**Risk Level**: {HIGH/MEDIUM/LOW} + +**Mitigation**: +- Feature flag allows instant rollback +- Comprehensive test coverage +- Breaking changes documented and addressed +- Gradual rollout prevents widespread impact + +## Additional Notes + +{Any additional context, concerns, or considerations} + +--- + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +### 7.3 Update Breaking Changes Section + +After creating PR, read `breaking-changes-analysis.md` and manually insert the detailed breaking changes into the PR description by editing it: + +```bash +gh pr edit --body "$(cat )" +``` + +Print after step completes: + +``` + • Branch pushed: + • PR URL: +✅ Step 7 complete: PR created and ready for review +``` + +## Step 8: Final Verification + +Print at start: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[STEP 8/8] FINAL VERIFICATION +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +### 8.1 Verify PR Created Successfully + +```bash +gh pr view --web +``` + +Check: + +- PR title is descriptive +- Breaking changes section is complete +- All checkboxes are present +- Test results are included +- Feature flag name is documented + +### 8.2 Report to User + +Print the final summary using exactly this format: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +✅ API VERSION UPGRADE COMPLETE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Destination: + Version: + Feature flag: + Branch: + PR: + + Test results: suites, tests — all passing + Breaking changes: identified and documented + + Next steps: + 1. Review PR and breaking-changes-analysis.md + 2. Manual smoke test with feature flag on/off + 3. Enable flag for internal testing + 4. Gradual rollout → promote canary to stable +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +## Troubleshooting + +### Common Issues + +**Issue**: Tests fail with authentication errors + +- **Solution**: Check if auth headers changed in new version +- **Action**: Update authentication implementation + +**Issue**: Tests fail with schema validation errors + +- **Solution**: Review request/response schema changes +- **Action**: Update payload types and validation + +**Issue**: Can't find current version + +- **Solution**: Search more broadly for version strings +- **Action**: Check all .ts files in destination directory + +**Issue**: Feature flag not working + +- **Solution**: Verify features parameter is passed through +- **Action**: Check extendRequest and perform functions + +**Issue**: Breaking changes analysis incomplete + +- **Solution**: Fetch additional documentation pages +- **Action**: Search for migration guides, release notes + +## Best Practices + +1. **Always create versioning-info.ts if it doesn't exist** - No exceptions +2. **Always use feature flags for version upgrades** - No direct version changes +3. **Always add tests for both versions** - Stable and canary +4. **Never skip breaking changes analysis** - Be thorough +5. **Run tests multiple times** to ensure consistency +6. **Document all assumptions** in PR description +7. **Keep stable version unchanged** until canary is validated +8. **Use descriptive feature flag names** with destination prefix +9. **Update changelog URL** in versioning-info.ts comments +10. **Plan gradual rollout** in PR description + +## Success Criteria + +Before marking complete, verify: + +- [ ] versioning-info.ts file exists (created or updated) +- [ ] Feature flag implemented correctly +- [ ] getApiVersion(features) helper function exists +- [ ] All API calls use getApiVersion(features) +- [ ] Tests for both stable and canary versions added +- [ ] All tests passing (100% pass rate) +- [ ] Breaking changes documented thoroughly +- [ ] PR created with comprehensive description +- [ ] Branch pushed to remote +- [ ] No merge conflicts +- [ ] Code review requested (automatic) +- [ ] User understands rollout plan + +## Reference Files + +The skill may need to read these for context: + +- `references/common-patterns.md` - Common destination patterns +- `references/feature-flags.md` - Feature flag best practices +- `references/testing-guide.md` - Testing strategies + +## Notes + +- **versioning-info.ts is MANDATORY** - Create it if it doesn't exist +- **Feature flags are MANDATORY** - All upgrades must be behind a flag +- **Tests for both versions are MANDATORY** - No exceptions +- Feature flags are managed by Segment's infrastructure team +- Reviewer assignment is automatic via CODEOWNERS +- Breaking changes analysis is the most critical step +- When in doubt, be more thorough, not less +- This workflow protects production systems - don't rush diff --git a/.claude/skills/api-version-upgrade/references/common-patterns.md b/.claude/skills/api-version-upgrade/references/common-patterns.md new file mode 100644 index 00000000000..7fa83891abe --- /dev/null +++ b/.claude/skills/api-version-upgrade/references/common-patterns.md @@ -0,0 +1,340 @@ +# Common API Versioning Patterns in Action Destinations + +This document catalogs the different patterns used for API versioning across Segment Action Destinations. + +## Pattern 1: Canary with versioning-info.ts (Preferred) + +**Used by**: Google Enhanced Conversions, The Trade Desk CRM + +**Structure**: + +``` +destination/ +├── versioning-info.ts # Version constants +├── utils.ts # Helper with getApiVersion() +└── actions/ + └── someAction/ + └── index.ts # Uses getApiVersion(features) +``` + +**versioning-info.ts**: + +```typescript +/** DESTINATION_API_VERSION + * Destination API version (stable/production). + * API reference: https://... + */ +export const DESTINATION_API_VERSION = 'v4' + +/** DESTINATION_CANARY_API_VERSION + * Destination API version (canary/feature-flagged). + */ +export const DESTINATION_CANARY_API_VERSION = 'v5' +``` + +**utils.ts** (or functions.ts): + +```typescript +import { Features } from '@segment/actions-core' +import { DESTINATION_API_VERSION, DESTINATION_CANARY_API_VERSION } from './versioning-info' + +export const API_VERSION = DESTINATION_API_VERSION +export const CANARY_API_VERSION = DESTINATION_CANARY_API_VERSION +export const FLAGON_NAME = '{destination-slug}-canary-version' + +export function getApiVersion(features?: Features): string { + return features && features[FLAGON_NAME] ? CANARY_API_VERSION : API_VERSION +} + +// Usage in API calls +export async function send(request, settings, payload, features) { + const version = getApiVersion(features) + const response = await request(`${BASE_URL}/${version}/endpoint`, { + method: 'POST', + json: payload + }) + return response +} +``` + +**index.ts** (destination definition): + +```typescript +extendRequest({ settings, features }) { + return { + headers: buildHeaders(settings.api_key, features) + } +} +``` + +**Advantages**: + +- Clean separation of version config +- Feature flag allows safe testing +- Easy to promote canary to stable +- Consistent pattern across destinations +- Gradual rollout capability + +## Pattern 2: Simple Constant in config.ts + +**Used by**: Klaviyo (before migration) + +**Structure**: + +``` +destination/ +├── config.ts # Version constant +├── functions.ts # Uses version in buildHeaders() +└── actions/ + └── someAction/ + └── index.ts # Uses buildHeaders() +``` + +**config.ts**: + +```typescript +export const API_URL = 'https://a.klaviyo.com/api' +export const REVISION_DATE = '2025-01-15' +``` + +**functions.ts**: + +```typescript +import { API_URL, REVISION_DATE } from './config' + +export function buildHeaders(authKey: string) { + return { + Authorization: `Klaviyo-API-Key ${authKey}`, + Accept: 'application/json', + revision: REVISION_DATE, // ← Used as HTTP header + 'Content-Type': 'application/json' + } +} +``` + +**index.ts**: + +```typescript +extendRequest({ settings }) { + return { + headers: buildHeaders(settings.api_key) + } +} +``` + +**Limitations**: + +- No feature flag support +- No canary testing capability +- Direct change to production version +- Harder to roll back if issues arise + +**Migration path**: Convert to Pattern 1 + +## Pattern 3: Inline Constants + +**Used by**: Some older destinations + +**Structure**: + +```typescript +// Directly in utils.ts or functions.ts +const API_VERSION = 'v3' +const BASE_URL = `https://api.example.com/${API_VERSION}` + +export async function sendData(request, payload) { + return await request(`${BASE_URL}/endpoint`, { + method: 'POST', + json: payload + }) +} +``` + +**Limitations**: + +- Hardcoded, scattered across codebase +- No feature flag support +- Difficult to test multiple versions +- Hard to track version changes + +**Migration path**: + +1. Extract to config.ts +2. Then migrate to Pattern 1 (versioning-info.ts) + +## Pattern 4: Version in URL Path vs Header + +Some APIs use version differently: + +### URL Path Version + +```typescript +const BASE_URL = `https://api.example.com/${API_VERSION}` +// Results in: https://api.example.com/v4/users +``` + +### Header Version (Klaviyo) + +```typescript +headers: { + 'revision': '2025-01-15' // ← Version as header +} +// URL stays: https://a.klaviyo.com/api/profiles/ +``` + +### Query Parameter Version + +```typescript +const url = `${BASE_URL}/endpoint?api-version=${API_VERSION}` +// Results in: https://api.example.com/endpoint?api-version=v4 +``` + +## Feature Flag Naming Convention + +**Pattern**: `{destination-slug}-canary-version` + +**Examples**: + +- `google-enhanced-canary-version` (Google Enhanced Conversions) +- `klaviyo-canary-version` (Klaviyo) +- `first-party-dv360-canary-version` (First-party DV360) +- `facebook-capi-actions-canary-version` (Facebook Conversions API) + +**Usage in code**: + +```typescript +export const FLAGON_NAME = '{destination-slug}-canary-version' + +// In actions +async perform(request, { payload, settings, features }) { + const version = getApiVersion(features) + // use version... +} +``` + +## Testing Both Versions + +**Standard pattern**: + +```typescript +import { FLAGON_NAME } from '../utils' + +describe('SomeAction', () => { + it('should use stable version by default', async () => { + const responses = await testDestination.testAction('actionName', { + event, + mapping, + settings + // features undefined = stable version + }) + + expect(mockRequest).toHaveBeenCalledWith(expect.stringContaining(API_VERSION)) + }) + + it('should use canary version with feature flag', async () => { + const responses = await testDestination.testAction('actionName', { + event, + mapping, + settings, + features: { [FLAGON_NAME]: true } // ← Enable canary + }) + + expect(mockRequest).toHaveBeenCalledWith(expect.stringContaining(CANARY_API_VERSION)) + }) +}) +``` + +## Migration Checklist: Simple → Canary Pattern + +When migrating from Pattern 2/3 to Pattern 1: + +- [ ] Create `versioning-info.ts` with both versions +- [ ] Export stable version with current value +- [ ] Export canary version with new value +- [ ] Add JSDoc comments with changelog URL +- [ ] Create `getApiVersion(features)` helper +- [ ] Define `FLAGON_NAME` constant +- [ ] Update `config.ts` to import from versioning-info +- [ ] Update all API call sites to use `getApiVersion()` +- [ ] Add `features` parameter to helper functions +- [ ] Update `extendRequest` to pass features +- [ ] Update action `perform` functions to receive features +- [ ] Add tests for both versions +- [ ] Verify feature flag toggles correctly +- [ ] Document in PR + +## Real-World Examples + +### Google Enhanced Conversions + +Located at `packages/destination-actions/src/destinations/google-enhanced-conversions/`. + +```typescript +// versioning-info.ts +export const GOOGLE_ENHANCED_CONVERSIONS_API_VERSION = 'v21' +export const GOOGLE_ENHANCED_CONVERSIONS_CANARY_API_VERSION = 'v21' + +// functions.ts +export const API_VERSION = GOOGLE_ENHANCED_CONVERSIONS_API_VERSION +export const CANARY_API_VERSION = GOOGLE_ENHANCED_CONVERSIONS_CANARY_API_VERSION +export const FLAGON_NAME = 'google-enhanced-canary-version' + +export function getApiVersion(features?: Features): string { + return features && features[FLAGON_NAME] ? CANARY_API_VERSION : API_VERSION +} + +// Usage +const version = getApiVersion(features) +const url = `https://googleads.googleapis.com/${version}/customers/...` +``` + +### The Trade Desk CRM + +```typescript +// versioning-info.ts +export const THE_TRADE_DESK_CRM_API_VERSION = 'v3' + +// functions.ts +const BASE_URL = `https://api.thetradedesk.com/${THE_TRADE_DESK_CRM_API_VERSION}` +export const TTD_LEGACY_FLOW_FLAG_NAME = 'actions-the-trade-desk-crm-legacy-flow' +``` + +### Klaviyo (Simple → Canary Migration) + +```typescript +// BEFORE (config.ts) +export const REVISION_DATE = '2025-01-15' + +// AFTER (versioning-info.ts) +export const KLAVIYO_API_VERSION = '2025-01-15' +export const KLAVIYO_CANARY_API_VERSION = '2026-01-15' + +// AFTER (config.ts) - maintain compatibility +import { KLAVIYO_API_VERSION } from './versioning-info' +export const REVISION_DATE = KLAVIYO_API_VERSION + +// AFTER (functions.ts) - add feature flag support +export const FLAGON_NAME = 'klaviyo-canary-version' +export function getApiRevision(features?: Features): string { + return features && features[FLAGON_NAME] ? KLAVIYO_CANARY_API_VERSION : KLAVIYO_API_VERSION +} + +export function buildHeaders(authKey: string, features?: Features) { + return { + Authorization: `Klaviyo-API-Key ${authKey}`, + Accept: 'application/json', + revision: getApiRevision(features), // ← Dynamic based on feature flag + 'Content-Type': 'application/json' + } +} +``` + +## Key Takeaways + +1. **Always use Pattern 1** (canary with versioning-info.ts) for new destinations +2. **Migrate older patterns** when doing version upgrades +3. **Feature flags are mandatory** for API version changes +4. **Test both versions** in your test suite +5. **Document changelog URL** in version constant comments +6. **Use consistent naming** for feature flags +7. **Pass features through** the entire call chain diff --git a/.claude/skills/api-version-upgrade/references/feature-flags.md b/.claude/skills/api-version-upgrade/references/feature-flags.md new file mode 100644 index 00000000000..dbcde8b2083 --- /dev/null +++ b/.claude/skills/api-version-upgrade/references/feature-flags.md @@ -0,0 +1,557 @@ +# Feature Flags Best Practices + +## Overview + +Feature flags (also called "feature toggles" or "feature switches") allow deploying code changes that can be toggled on/off without redeploying. For API version upgrades, they enable safe, gradual rollouts with instant rollback capability. + +## Why Feature Flags for API Upgrades? + +### Risk Mitigation + +- **Instant rollback** - Disable problematic version immediately +- **Gradual rollout** - Test with small percentage before full deployment +- **A/B testing** - Compare performance between versions +- **Safe production testing** - Test in production with real traffic +- **Zero-downtime upgrades** - Switch versions without redeployment + +### Business Benefits + +- **Reduced deployment risk** - Can deploy anytime without fear +- **Faster iteration** - No need to wait for perfect implementation +- **Customer segmentation** - Roll out to specific customers first +- **Emergency brake** - Quick mitigation if issues arise + +## Feature Flag Lifecycle + +``` +┌─────────────┐ +│ Develop │ Create code with flag, default OFF +└──────┬──────┘ + │ +┌──────▼──────┐ +│ Merge │ Flag in codebase, still OFF in production +└──────┬──────┘ + │ +┌──────▼──────┐ +│ Deploy │ Code deployed, flag still OFF (no risk) +└──────┬──────┘ + │ +┌──────▼──────┐ +│ Test (1%) │ Enable for 1% of traffic, monitor closely +└──────┬──────┘ + │ +┌──────▼──────┐ +│ Expand (10%)│ Gradually increase percentage +└──────┬──────┘ + │ +┌──────▼──────┐ +│ Full (100%) │ Enable for all traffic +└──────┬──────┘ + │ +┌──────▼──────┐ +│ Promote │ Make new version default (stable) +└──────┬──────┘ + │ +┌──────▼──────┐ +│ Cleanup │ Remove flag and old code after validation period +└─────────────┘ +``` + +## Implementation Pattern + +### Standard Structure + +**versioning-info.ts**: + +```typescript +/** DESTINATION_API_VERSION + * Destination API version (stable/production). + * Used by default when feature flag is not enabled. + * Changelog: https://... + */ +export const DESTINATION_API_VERSION = 'v4' + +/** DESTINATION_CANARY_API_VERSION + * Destination API version (canary/testing). + * Used when feature flag is enabled. + * Testing migration from v4 to v5. + */ +export const DESTINATION_CANARY_API_VERSION = 'v5' +``` + +**utils.ts** (or functions.ts): + +```typescript +import { Features } from '@segment/actions-core' +import { DESTINATION_API_VERSION, DESTINATION_CANARY_API_VERSION } from './versioning-info' + +export const API_VERSION = DESTINATION_API_VERSION +export const CANARY_API_VERSION = DESTINATION_CANARY_API_VERSION + +/** Feature flag name for canary API version testing */ +export const FLAGON_NAME = '{destination-slug}-canary-version' + +export function getApiVersion(features?: Features): string { + return features && features[FLAGON_NAME] ? CANARY_API_VERSION : API_VERSION +} +``` + +### Usage in Code + +**Action implementation**: + +```typescript +export default { + perform: async (request, { payload, settings, features }) => { + const version = getApiVersion(features) + + const response = await request(`https://api.example.com/${version}/endpoint`, { + method: 'POST', + json: payload + }) + + return response + } +} +``` + +**Shared utilities**: + +```typescript +export function buildHeaders(authKey: string, features?: Features) { + const version = getApiVersion(features) + + return { + Authorization: `Bearer ${authKey}`, + 'X-API-Version': version, + 'Content-Type': 'application/json' + } +} +``` + +**Request extension**: + +```typescript +export default { + extendRequest({ settings, features }) { + return { + headers: buildHeaders(settings.api_key, features) + } + } +} +``` + +## Naming Conventions + +### Feature Flag Names + +**Format**: `{destination-slug}-canary-version` + +**Examples**: + +- `klaviyo-canary-version` +- `google-enhanced-canary-version` +- `facebook-capi-actions-canary-version` +- `first-party-dv360-canary-version` + +**Rules**: + +- Use destination slug (kebab-case) +- Always suffix with `-canary-version` +- Keep consistent across all destinations +- Document in code as `FLAGON_NAME` constant + +### Constant Names + +**versioning-info.ts**: + +```typescript +// Pattern: {DESTINATION}_API_VERSION +// Date-based revision APIs (e.g. Klaviyo) +export const KLAVIYO_API_REVISION = '2025-01-15' +export const KLAVIYO_CANARY_API_REVISION = '2026-01-15' + +// Numeric version APIs (e.g. Google CM360) +export const GOOGLE_CM360_API_VERSION = 'v4' +export const GOOGLE_CM360_CANARY_API_VERSION = 'v5' +``` + +**Function names**: + +```typescript +// Pattern: getApiVersion +export function getApiVersion(features?: Features): string + +// Alternative patterns (less common): +export function getRevision(features?: Features): string +export function getVersion(features?: Features): string +``` + +## Testing with Feature Flags + +### Test Structure + +```typescript +import { FLAGON_NAME, API_VERSION, CANARY_API_VERSION } from '../utils' +import { createTestIntegration, createTestEvent } from '@segment/actions-core' + +const testDestination = createTestIntegration(Definition) + +describe('API Version Feature Flag', () => { + describe('Stable Version (default)', () => { + it('should use stable version when flag not set', async () => { + const scope = nock('https://api.example.com').post(`/${API_VERSION}/endpoint`).reply(200, { success: true }) + + await testDestination.testAction('actionName', { + event: createTestEvent({}), + mapping: { field: 'value' }, + settings: { api_key: 'test' } + // features not provided = stable version + }) + + expect(scope.isDone()).toBeTruthy() + }) + + it('should use stable version when flag explicitly false', async () => { + const scope = nock('https://api.example.com').post(`/${API_VERSION}/endpoint`).reply(200, { success: true }) + + await testDestination.testAction('actionName', { + event: createTestEvent({}), + mapping: { field: 'value' }, + settings: { api_key: 'test' }, + features: { [FLAGON_NAME]: false } // Explicitly disabled + }) + + expect(scope.isDone()).toBeTruthy() + }) + }) + + describe('Canary Version (feature flag enabled)', () => { + it('should use canary version when flag enabled', async () => { + const scope = nock('https://api.example.com') + .post(`/${CANARY_API_VERSION}/endpoint`) + .reply(200, { success: true }) + + await testDestination.testAction('actionName', { + event: createTestEvent({}), + mapping: { field: 'value' }, + settings: { api_key: 'test' }, + features: { [FLAGON_NAME]: true } // ← Enable canary + }) + + expect(scope.isDone()).toBeTruthy() + }) + + it('should handle canary-specific response format', async () => { + nock('https://api.example.com') + .post(`/${CANARY_API_VERSION}/endpoint`) + .reply(200, { + // New response format in canary + result: { status: 'success' }, + metadata: { version: CANARY_API_VERSION } + }) + + const responses = await testDestination.testAction('actionName', { + event: createTestEvent({}), + mapping: { field: 'value' }, + settings: { api_key: 'test' }, + features: { [FLAGON_NAME]: true } + }) + + expect(responses[0].status).toBe(200) + expect(responses[0].data.metadata.version).toBe(CANARY_API_VERSION) + }) + }) + + describe('Feature Flag Isolation', () => { + it('should not affect other feature flags', async () => { + const scope = nock('https://api.example.com').post(`/${CANARY_API_VERSION}/endpoint`).reply(200, {}) + + await testDestination.testAction('actionName', { + event: createTestEvent({}), + mapping: { field: 'value' }, + settings: { api_key: 'test' }, + features: { + [FLAGON_NAME]: true, // Enable this flag + 'other-feature-flag': false, // Other flags unaffected + 'another-flag': true + } + }) + + expect(scope.isDone()).toBeTruthy() + }) + }) +}) +``` + +### Test Best Practices + +1. **Test both states** - Always test with flag on AND off +2. **Test default behavior** - Verify stable version is default +3. **Test explicit false** - Verify false value works same as undefined +4. **Test isolation** - Verify flag doesn't affect unrelated behavior +5. **Test edge cases** - Invalid flag values, missing features object +6. **Test combinations** - Multiple feature flags together + +## Rollout Strategy + +### Phase 1: Development (0%) + +- Code merged with flag OFF +- Feature in production but inactive +- No user impact +- **Duration**: Immediate (with PR merge) + +### Phase 2: Internal Testing (0.1%) + +- Enable for Segment internal workspaces +- Monitor errors, latency, correctness +- Gather feedback from internal users +- **Duration**: 1-3 days + +### Phase 3: Beta Customers (1-5%) + +- Enable for select early-adopter customers +- Monitor closely for issues +- Collect feedback and metrics +- **Duration**: 3-7 days + +### Phase 4: Gradual Rollout (5-50%) + +- Incrementally increase percentage +- Monitor error rates, performance +- Steps: 5% → 10% → 25% → 50% +- **Duration**: 7-14 days + +### Phase 5: Majority Rollout (50-95%) + +- Most users now on new version +- Old version still available as fallback +- Continue monitoring +- **Duration**: 7-14 days + +### Phase 6: Full Rollout (100%) + +- All users on new version +- Keep flag in case rollback needed +- Validate everything works +- **Duration**: 7-30 days (observation period) + +### Phase 7: Promotion + +- Make new version the stable default +- Update `DESTINATION_API_VERSION` to canary value +- Prepare new canary for next upgrade +- **Duration**: Immediate (code change) + +### Phase 8: Cleanup + +- Remove feature flag logic +- Remove old version constant +- Clean up conditional code +- **Duration**: 1-2 weeks (next sprint) + +## Monitoring and Metrics + +### Key Metrics to Track + +**Error Rates**: + +``` +error_rate_stable = errors_stable / requests_stable +error_rate_canary = errors_canary / requests_canary +delta = error_rate_canary - error_rate_stable +``` + +**Latency**: + +``` +p50_stable vs p50_canary +p95_stable vs p95_canary +p99_stable vs p99_canary +``` + +**Success Rates**: + +``` +success_rate = successful_requests / total_requests +compare: stable vs canary +``` + +**Request Volume**: + +``` +requests_per_minute_stable +requests_per_minute_canary +total_requests (both combined) +``` + +### Alert Thresholds + +**Error rate spike**: + +- **Warning**: Canary error rate > Stable error rate + 5% +- **Critical**: Canary error rate > Stable error rate + 10% +- **Action**: Investigate immediately, prepare rollback + +**Latency degradation**: + +- **Warning**: Canary p95 > Stable p95 + 20% +- **Critical**: Canary p95 > Stable p95 + 50% +- **Action**: Analyze slow requests, consider rollback + +**Success rate drop**: + +- **Warning**: Canary success rate < Stable success rate - 2% +- **Critical**: Canary success rate < Stable success rate - 5% +- **Action**: Rollback immediately + +### Rollback Triggers + +Automatic rollback if: + +1. Canary error rate > 10% absolute +2. Canary error rate > 2x stable error rate +3. Canary success rate < 90% +4. Multiple critical alerts in short timeframe +5. Customer escalation for canary users + +## Documentation Requirements + +### PR Description Template + +```markdown +## Feature Flag Details + +**Flag Name**: `{destination-slug}-canary-version` (e.g. `google-enhanced-canary-version`) + +**Stable Version**: v4 +**Canary Version**: v5 + +**Default State**: OFF (stable version) + +**Rollout Plan**: + +- Phase 1: Internal testing (0.1%) +- Phase 2: Beta customers (1-5%) +- Phase 3: Gradual rollout (5-50%) +- Phase 4: Full rollout (100%) +- Phase 5: Promotion to stable +- Phase 6: Cleanup + +**Monitoring**: + +- Error rates (stable vs canary) +- Latency (p50, p95, p99) +- Success rates +- Request volume + +**Rollback Criteria**: + +- Error rate > 10% +- Success rate < 90% +- Customer escalation +- Critical alerts + +**Breaking Changes**: See below + +## Breaking Changes + +[... detailed analysis ...] +``` + +### Code Comments + +```typescript +/** DESTINATION_CANARY_API_VERSION + * Canary API version for gradual rollout testing. + * + * This version is behind the feature flag 'destination-canary-version'. + * + * Rollout status: [UPDATE THIS] + * - Phase: Internal testing + * - Enabled: 0.1% of traffic + * - Started: 2026-03-14 + * - Expected full rollout: 2026-04-14 + * + * Known issues: None + * Breaking changes: See breaking-changes-analysis.md + */ +export const DESTINATION_CANARY_API_VERSION = 'v5' +``` + +## Common Pitfalls + +### ❌ Don't: Default to new version + +```typescript +// WRONG - New version is default +export function getApiVersion(features?: Features): string { + return features && features[FLAGON_NAME] ? API_VERSION : CANARY_API_VERSION +} +``` + +### ✅ Do: Default to stable version + +```typescript +// CORRECT - Stable version is default +export function getApiVersion(features?: Features): string { + return features && features[FLAGON_NAME] ? CANARY_API_VERSION : API_VERSION +} +``` + +### ❌ Don't: Forget to pass features through + +```typescript +// WRONG - features lost in chain +export function buildHeaders(authKey: string) { + return { + 'X-API-Version': API_VERSION // Always stable! + } +} +``` + +### ✅ Do: Thread features through call chain + +```typescript +// CORRECT - features passed through +export function buildHeaders(authKey: string, features?: Features) { + return { + 'X-API-Version': getApiVersion(features) + } +} +``` + +### ❌ Don't: Remove flag too quickly + +```typescript +// WRONG - Remove flag same day as 100% rollout +// What if there's a production issue tomorrow? +``` + +### ✅ Do: Keep flag for observation period + +```typescript +// CORRECT - Keep flag 2-4 weeks after 100% +// Allows safe rollback if issues arise +``` + +## Best Practices Summary + +1. **Always default to stable** - New version should require opt-in +2. **Test both versions** - Comprehensive test coverage for both paths +3. **Document thoroughly** - Clear comments, PR descriptions, rollout plans +4. **Monitor closely** - Track metrics during rollout +5. **Rollout gradually** - Don't jump from 0% to 100% +6. **Keep escape hatch** - Maintain ability to rollback +7. **Plan promotion** - Have strategy for making canary stable +8. **Schedule cleanup** - Don't leave flags forever +9. **Communicate status** - Keep team informed of rollout progress +10. **Learn from incidents** - Document issues and improvements + +## Resources + +- **Feature Flags at Segment**: [Internal docs link] +- **Rollout best practices**: [Internal docs link] +- **Monitoring dashboards**: [Dashboard links] +- **On-call runbooks**: [Runbook links] diff --git a/.claude/skills/api-version-upgrade/references/testing-guide.md b/.claude/skills/api-version-upgrade/references/testing-guide.md new file mode 100644 index 00000000000..fa806f38199 --- /dev/null +++ b/.claude/skills/api-version-upgrade/references/testing-guide.md @@ -0,0 +1,534 @@ +# Testing Guide for API Version Upgrades + +## Overview + +This guide covers testing strategies for API version upgrades in Segment Action Destinations, including running tests, interpreting results, and handling failures. + +## Test Organization + +### Directory Structure + +``` +packages/destination-actions/src/destinations/{destination}/ +├── __tests__/ +│ ├── index.test.ts # Main destination tests +│ ├── snapshot.test.ts # Snapshot tests +│ └── multistatus.test.ts # Multi-status response tests +├── {action1}/ +│ └── __tests__/ +│ ├── index.test.ts # Action-specific tests +│ └── snapshot.test.ts # Action snapshots +└── {action2}/ + └── __tests__/ + └── index.test.ts +``` + +## Running Tests + +### Prerequisites + +#### Node Version + +Action destinations require Node 18.17+ or 22.13+: + +```bash +# Check current version +node --version + +# Switch if needed (nvm) +source ~/.nvm/nvm.sh +nvm use 22.13.1 + +# Verify +node --version # Should show v22.13.1 +``` + +### Test Commands + +#### Run All Destination Tests + +```bash +# From repository root +TZ=UTC yarn cloud test --testPathPattern=src/destinations/{destination} --no-coverage +``` + +#### Run Specific Test File + +```bash +TZ=UTC yarn cloud test --testPathPattern=src/destinations/{destination}/__tests__/index.test.ts +``` + +#### Run a Single Test Case (IDE-style or targeted) + +IDEs (VS Code, JetBrains) generate this form when you click "Run test" on a specific case. You can also run it manually: + +```bash +node packages/destination-actions/node_modules/jest/bin/jest.js \ + 'packages/destination-actions/src/destinations/{destination}/{action}/__tests__/index.test.ts' \ + -t '^{DescribeName} {testName}(\s.*)?$' +``` + +This calls the package-local Jest binary directly, bypassing yarn workspaces. Useful for a single test during active development, but use `yarn cloud test` for full destination runs to ensure correct workspace config. + +#### Run With Coverage + +```bash +TZ=UTC yarn cloud test --testPathPattern=src/destinations/{destination} --coverage +``` + +#### Run in Watch Mode + +```bash +TZ=UTC yarn cloud test --testPathPattern=src/destinations/{destination} --watch +``` + +### Auto-Detecting Test Pattern + +The skill should automatically detect the destination name and construct the pattern: + +```typescript +// Given destination name: "klaviyo" +const testPattern = `src/destinations/${destinationName}` +// Results in: src/destinations/klaviyo + +// Run tests +const command = `TZ=UTC yarn cloud test --testPathPattern=${testPattern} --no-coverage` +``` + +## Test Output Interpretation + +### Success Output + +``` +PASS src/destinations/klaviyo/__tests__/index.test.ts (20.435 s) +PASS src/destinations/klaviyo/upsertProfile/__tests__/index.test.ts (21.627 s) + +Test Suites: 14 passed, 14 total +Tests: 133 passed, 133 total +Snapshots: 24 passed, 24 total +Time: 26.888 s +``` + +**Green flags**: + +- All test suites passed +- All tests passed +- All snapshots passed +- Exit code 0 + +### Failure Output + +``` +FAIL src/destinations/klaviyo/upsertProfile/__tests__/index.test.ts + ● upsertProfile › should create profile with new API version + + Expected request to be called with URL containing "2026-01-15" + but got "2025-01-15" + + 102 | expect(mockRequest).toHaveBeenCalledWith( + 103 | expect.stringContaining('2026-01-15') + > 104 | ) +``` + +**Red flags**: + +- FAIL markers +- Expected vs Received mismatches +- Snapshot mismatches +- Exit code 1 + +### Common Test Patterns + +#### Testing Feature Flag Toggle + +```typescript +import { FLAGON_NAME, API_VERSION, CANARY_API_VERSION } from '../utils' + +describe('API Version Feature Flag', () => { + const testDestination = createTestIntegration(Definition) + + beforeEach(() => { + nock('https://api.example.com') + .post(/v4|v5/) // Accept both versions + .reply(200, { success: true }) + }) + + it('uses stable version by default', async () => { + await testDestination.testAction('actionName', { + event: createTestEvent({}), + mapping: { field: 'value' }, + settings: { api_key: 'test-key' } + // No features parameter = stable version + }) + + expect(nock.pendingMocks()).toHaveLength(0) + // Verify stable version was called + }) + + it('uses canary version when feature flag enabled', async () => { + await testDestination.testAction('actionName', { + event: createTestEvent({}), + mapping: { field: 'value' }, + settings: { api_key: 'test-key' }, + features: { [FLAGON_NAME]: true } // ← Enable canary + }) + + expect(nock.pendingMocks()).toHaveLength(0) + // Verify canary version was called + }) +}) +``` + +#### Testing Request URL + +```typescript +it('should call correct API endpoint with canary version', async () => { + const scope = nock('https://api.example.com') + .post('/v5/users') // Canary version + .reply(200, { id: '123' }) + + await testDestination.testAction('actionName', { + event, + mapping, + settings, + features: { [FLAGON_NAME]: true } + }) + + expect(scope.isDone()).toBeTruthy() +}) +``` + +#### Testing Request Headers + +```typescript +it('should send correct revision header with canary version', async () => { + const scope = nock('https://api.example.com') + .post('/endpoint') + .matchHeader('revision', '2026-01-15') // Canary revision + .reply(200, {}) + + await testDestination.testAction('actionName', { + event, + mapping, + settings, + features: { [FLAGON_NAME]: true } + }) + + expect(scope.isDone()).toBeTruthy() +}) +``` + +#### Testing Response Handling + +```typescript +it('should handle canary version response format', async () => { + nock('https://api.example.com') + .post('/v5/users') + .reply(200, { + data: { id: '123', status: 'active' }, + // New response format in canary + metadata: { version: 'v5' } + }) + + const responses = await testDestination.testAction('actionName', { + event, + mapping, + settings, + features: { [FLAGON_NAME]: true } + }) + + expect(responses[0].status).toBe(200) + expect(responses[0].data).toHaveProperty('metadata') +}) +``` + +## Handling Test Failures + +### Failure Analysis Workflow + +1. **Read the error message carefully** + + - What was expected? + - What was received? + - Which line failed? + +2. **Identify the root cause** + + - Breaking API change? + - Implementation bug? + - Test needs updating? + +3. **Check breaking changes analysis** + + - Does this align with documented breaking changes? + - Is this an expected difference? + +4. **Fix and retest** + - Update implementation OR + - Update test expectations OR + - Both + +### Common Failure Scenarios + +#### Scenario 1: URL Format Changed + +**Error**: + +``` +Expected URL: https://api.example.com/v5/users +Received URL: https://api.example.com/v5/user +``` + +**Cause**: API endpoint renamed (users → user) + +**Fix**: Update endpoint in implementation: + +```typescript +// Before +const url = `${BASE_URL}/${version}/users` + +// After +const url = `${BASE_URL}/${version}/user` // Singular +``` + +#### Scenario 2: Required Field Added + +**Error**: + +``` +API returned 400: Missing required field 'client_id' +``` + +**Cause**: New API version requires additional field + +**Fix**: Add field to request: + +```typescript +// Before +const payload = { + user_id: userId, + email: email +} + +// After +const payload = { + user_id: userId, + email: email, + client_id: settings.client_id // New required field +} +``` + +#### Scenario 3: Response Schema Changed + +**Error**: + +``` +TypeError: Cannot read property 'data' of undefined +``` + +**Cause**: Response structure changed + +**Fix**: Update response handling: + +```typescript +// Before +const userId = response.data.id + +// After +const userId = response.result?.user?.id // New structure +``` + +#### Scenario 4: Authentication Method Changed + +**Error**: + +``` +401 Unauthorized: Invalid authentication method +``` + +**Cause**: API changed from API key to Bearer token + +**Fix**: Update authentication: + +```typescript +// Before +headers: { + 'X-API-Key': settings.api_key +} + +// After +headers: { + 'Authorization': `Bearer ${settings.api_token}` +} +``` + +### Snapshot Test Updates + +If snapshots need updating (when changes are intentional): + +```bash +# Update snapshots +TZ=UTC yarn cloud test --testPathPattern=src/destinations/{destination} --updateSnapshot + +# Review changes +git diff +``` + +**⚠️ Warning**: Only update snapshots if you understand and approve the changes. Don't blindly update. + +## Testing Checklist + +Before marking tests as complete: + +### Pre-Test + +- [ ] Node version is compatible (18.17+ or 22.13+) +- [ ] All dependencies installed (`yarn install`) +- [ ] Working directory is clean +- [ ] No other tests running (can cause conflicts) + +### During Test + +- [ ] All test suites pass +- [ ] All individual tests pass +- [ ] All snapshots match +- [ ] No warnings or deprecation notices +- [ ] Tests run in reasonable time (<5 min) + +### Post-Test + +- [ ] Stable version tests pass (no feature flag) +- [ ] Canary version tests pass (with feature flag) +- [ ] Both versions tested in same test run +- [ ] No test files were skipped +- [ ] Coverage is adequate (if checking) + +### Breaking Changes Validation + +- [ ] Each documented breaking change has a test +- [ ] Tests verify new behavior works correctly +- [ ] Tests verify old behavior still works (stable version) +- [ ] Edge cases are covered + +## Debugging Test Failures + +### Enable Verbose Output + +```bash +TZ=UTC yarn cloud test --testPathPattern=src/destinations/{destination} --verbose +``` + +### Run Single Test + +```bash +TZ=UTC yarn cloud test --testPathPattern=src/destinations/{destination} --testNamePattern="specific test name" +``` + +### Add Debug Logging + +Temporarily add console.log in tests: + +```typescript +it('should do something', async () => { + console.log('Request:', JSON.stringify(mockRequest.mock.calls, null, 2)) + console.log('Response:', JSON.stringify(responses, null, 2)) + // ... test assertions +}) +``` + +### Check Nock Mocks + +```typescript +afterEach(() => { + if (!nock.isDone()) { + console.log('Pending mocks:', nock.pendingMocks()) + } + nock.cleanAll() +}) +``` + +### Validate Request Matching + +```typescript +// Be specific with nock matchers +nock('https://api.example.com') + .post('/v5/users', (body) => { + console.log('Request body:', body) + return true // Return false to reject + }) + .reply(200, {}) +``` + +## Performance Considerations + +### Timeouts + +Increase timeout for slow tests: + +```typescript +it('should handle large batch', async () => { + // ... test +}, 30000) // 30 second timeout +``` + +### Parallel Execution + +Tests run in parallel by default. If this causes issues: + +```bash +TZ=UTC yarn cloud test --testPathPattern=src/destinations/{destination} --runInBand +``` + +### Test Isolation + +Ensure tests don't affect each other: + +```typescript +beforeEach(() => { + // Reset state + nock.cleanAll() +}) + +afterEach(() => { + // Clean up + nock.cleanAll() +}) +``` + +## CI/CD Integration + +Tests must pass in CI before merge: + +### Local Pre-Push Check + +```bash +# Run what CI will run +TZ=UTC yarn cloud test --testPathPattern=src/destinations/{destination} --no-coverage --ci + +# Check exit code +echo $? # Should be 0 +``` + +### CI Environment Differences + +CI environments may: + +- Use different Node versions +- Have stricter timeout limits +- Run more tests in parallel +- Have network restrictions + +Test locally in conditions similar to CI when possible. + +## Best Practices + +1. **Test both versions** - Always verify stable and canary work +2. **Use realistic data** - Test with production-like payloads +3. **Test error cases** - Don't just test happy paths +4. **Keep tests focused** - One thing per test +5. **Use descriptive names** - Test names should explain what's tested +6. **Clean up mocks** - Prevent test interference +7. **Document assumptions** - Explain why tests are written that way +8. **Run multiple times** - Ensure tests aren't flaky +9. **Check coverage** - Ensure new code paths are tested +10. **Update regularly** - Keep tests in sync with implementation