Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
939 changes: 939 additions & 0 deletions .claude/skills/api-version-upgrade/SKILL.md

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions .claude/skills/api-version-upgrade/evals/evals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"skill_name": "api-version-upgrade",
"evals": [
{
"id": 1,
"name": "klaviyo-simple-to-canary-migration",
"prompt": "Upgrade Klaviyo destination from version 2025-01-15 to 2026-01-15. The destination currently uses a simple REVISION_DATE constant in config.ts without feature flags. Please migrate it to use the canary pattern with feature flags, analyze breaking changes from the Klaviyo API changelog (https://developers.klaviyo.com/en/docs/changelog), run all tests, and create a PR. The Klaviyo API uses the revision date as an HTTP header.",
"expected_output": "Complete upgrade with:\n- versioning-info.ts created with both versions\n- config.ts updated to import from versioning-info\n- buildHeaders() updated to accept features parameter\n- getApiRevision() helper function created\n- extendRequest updated to pass features\n- All tests passing for both versions\n- Breaking changes analysis document\n- PR created with detailed description\n- Branch pushed to origin",
"files": [],
"assertions": []
},
{
"id": 2,
"name": "google-cm360-canary-update",
"prompt": "hey can you bump Google Campaign Manager 360 from v4 to v5? they already have the canary pattern set up with versioning-info.ts and feature flags. just need to update the CANARY version from v5 to v6 and check for breaking changes. their changelog is at https://developers.google.com/doubleclick-advertisers/deprecation. run tests and push a pr when done",
"expected_output": "Complete upgrade with:\n- GOOGLE_CM360_CANARY_API_VERSION updated to 'v6'\n- GOOGLE_CM360_API_VERSION remains 'v4' (stable)\n- Breaking changes fetched from Google docs\n- Comprehensive analysis of v4→v6 changes\n- All tests passing (stable and canary)\n- Feature flag tests verified\n- PR created with breaking changes\n- Branch created and pushed",
"files": [],
"assertions": []
},
{
"id": 3,
"name": "new-destination-first-version",
"prompt": "We're adding a new destination called 'amplitude-web' that uses API version 'v2' from https://amplitude.com/api/v2. It currently has the version hardcoded in utils.ts as a constant. Can you set up the versioning structure properly with v1 as stable and v2 as canary behind a feature flag? Also check https://amplitude.com/docs/apis/api-changelog for breaking changes between v1 and v2.",
"expected_output": "Complete setup with:\n- versioning-info.ts created from scratch\n- AMPLITUDE_WEB_API_VERSION = 'v1'\n- AMPLITUDE_WEB_CANARY_API_VERSION = 'v2'\n- getApiVersion() helper created\n- FLAGON_NAME defined\n- All API call sites updated to use helper\n- Tests added for both versions\n- Breaking changes analysis\n- PR with full implementation",
"files": [],
"assertions": []
}
]
}
334 changes: 334 additions & 0 deletions .claude/skills/api-version-upgrade/references/common-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
# 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 Campaign Manager 360, 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-canary-api-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-api-version`

**Examples**:

- `cm360-canary-api-version` (Google Campaign Manager 360)
- `klaviyo-canary-api-version` (Klaviyo)
- `the-trade-desk-crm-canary-api-version` (The Trade Desk CRM)

**Usage in code**:

```typescript
export const FLAGON_NAME = 'destination-canary-api-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 Campaign Manager 360

```typescript
// versioning-info.ts
export const GOOGLE_CM360_API_VERSION = 'v4' // stable
export const GOOGLE_CM360_CANARY_API_VERSION = 'v5' // canary

// utils.ts
export const FLAGON_NAME = 'cm360-canary-api-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://dfareporting.googleapis.com/dfareporting/${version}/userprofiles/...`
```

### 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-api-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
Loading
Loading