Skip to content
Open
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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,44 @@ rules; existing integrations do not need to change.
`valueType` while the environment still uses the legacy target contract. Narrow
using `'ruleId' in target` before accessing the contract-specific fields.

## Feature flag events

The Events API and `webhooks.constructEvent` support the namespaced feature flag
lifecycle events:

- `feature_flags.flag.created`, `feature_flags.flag.updated`, `feature_flags.flag.deleted`
- `feature_flags.flag_rule.created`, `feature_flags.flag_rule.updated`, `feature_flags.flag_rule.deleted`
- `feature_flags.flag_target.created`, `feature_flags.flag_target.deleted`

Use these names in `events.listEvents({ events: [...] })`. The returned `Event`
union narrows by `event`; event data and context use camelCase fields.

```ts
const { data: events } = await workos.events.listEvents({
events: [
'feature_flags.flag_target.created',
'feature_flags.flag_rule.updated',
],
});

for (const event of events) {
if (event.event === 'feature_flags.flag_target.created') {
if ('ruleId' in event.data) {
console.log(event.data.ruleId, event.data.targetId);
} else {
console.log(event.data.valueType, event.data.value);
}
}
}
```

Flag events include `environmentId`, nullable `description`, and a nullable
`owner` with `email`, `firstName`, and `lastName`. Updated flag context includes
optional `previousAttributes.data`, and updated rule context includes the rule's
previous `value` there. Rule context can omit `actor` for system changes. Target
event data is a `FlagTarget` with `ruleId`, or a `LegacyFlagTarget` with `value`
and `valueType`. The `flag.*` events keep their existing data shape.

## SDK Versioning

For our SDKs WorkOS follows a Semantic Versioning ([SemVer](https://semver.org/)) process where all releases will have a version X.Y.Z (like 1.0.0) pattern wherein Z would be a bug fix (e.g., 1.0.1), Y would be a minor release (1.1.0) and X would be a major release (2.0.0). We permit any breaking changes to only be released in major versions and strongly recommend reading changelogs before making any major version upgrades.
Expand Down
186 changes: 186 additions & 0 deletions src/common/interfaces/event.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ import { ApiKey, SerializedApiKey } from '../../api-keys/interfaces';
import {
FeatureFlag,
FeatureFlagResponse,
FlagRule,
FlagRuleResponse,
FlagTargetResource,
FlagTargetResourceResponse,
} from '../../feature-flags/interfaces';
import { Group, GroupResponse } from '../../groups/interfaces';
import { ConnectedAccountState } from '../../pipes/interfaces';
Expand Down Expand Up @@ -718,6 +722,172 @@ export interface ApiKeyRevokedEventResponse extends EventResponseBase {
data: SerializedApiKey;
}

export type FeatureFlagEventData = Omit<FeatureFlag, 'description'> & {
environmentId: string;
description: string | null;
owner: {
email: string;
firstName: string | null;
lastName: string | null;
} | null;
};

export type FeatureFlagEventResponseData = Omit<
FeatureFlagResponse,
'description'
> & {
environment_id: string;
description: string | null;
owner: {
email: string;
first_name: string | null;
last_name: string | null;
} | null;
};

export type FeatureFlagActor = {
id: string;
source: 'api' | 'dashboard' | 'admin_portal' | 'system';
name: string | null;
};

export type FeatureFlagContext = { clientId: string; actor: FeatureFlagActor };
export type FeatureFlagContextResponse = {
client_id: string;
actor: FeatureFlagActor;
};
export type FeatureFlagRuleContext = {
clientId: string;
actor?: FeatureFlagActor;
};
export type FeatureFlagRuleContextResponse = {
client_id: string;
actor?: FeatureFlagActor;
};
export type FeatureFlagRuleUpdatedContext = FeatureFlagRuleContext & {
previousAttributes?: { data?: Partial<Pick<FlagRule, 'value'>> };
};
export type FeatureFlagRuleUpdatedContextResponse =
FeatureFlagRuleContextResponse & {
previous_attributes?: { data?: Partial<Pick<FlagRuleResponse, 'value'>> };
};
export type FeatureFlagUpdatedContext = FeatureFlagContext & {
previousAttributes?: {
data?: Partial<
Pick<
FeatureFlagEventData,
'name' | 'description' | 'tags' | 'owner' | 'enabled' | 'defaultValue'
>
>;
};
};
export type FeatureFlagUpdatedContextResponse = FeatureFlagContextResponse & {
previous_attributes?: {
data?: Partial<
Pick<
FeatureFlagEventResponseData,
'name' | 'description' | 'tags' | 'owner' | 'enabled' | 'default_value'
>
>;
};
};

export interface FeatureFlagsFlagCreatedEvent extends EventBase {
event: 'feature_flags.flag.created';
data: FeatureFlagEventData;
context: FeatureFlagContext;
}

export interface FeatureFlagsFlagCreatedEventResponse extends EventResponseBase {
event: 'feature_flags.flag.created';
data: FeatureFlagEventResponseData;
context: FeatureFlagContextResponse;
}

export interface FeatureFlagsFlagUpdatedEvent extends EventBase {
event: 'feature_flags.flag.updated';
data: FeatureFlagEventData;
context: FeatureFlagUpdatedContext;
}

export interface FeatureFlagsFlagUpdatedEventResponse extends EventResponseBase {
event: 'feature_flags.flag.updated';
data: FeatureFlagEventResponseData;
context: FeatureFlagUpdatedContextResponse;
}

export interface FeatureFlagsFlagDeletedEvent extends EventBase {
event: 'feature_flags.flag.deleted';
data: FeatureFlagEventData;
context: FeatureFlagContext;
}

export interface FeatureFlagsFlagDeletedEventResponse extends EventResponseBase {
event: 'feature_flags.flag.deleted';
data: FeatureFlagEventResponseData;
context: FeatureFlagContextResponse;
}

export interface FeatureFlagsFlagRuleCreatedEvent extends EventBase {
event: 'feature_flags.flag_rule.created';
data: FlagRule;
context: FeatureFlagRuleContext;
}

export interface FeatureFlagsFlagRuleCreatedEventResponse extends EventResponseBase {
event: 'feature_flags.flag_rule.created';
data: FlagRuleResponse;
context: FeatureFlagRuleContextResponse;
}

export interface FeatureFlagsFlagRuleUpdatedEvent extends EventBase {
event: 'feature_flags.flag_rule.updated';
data: FlagRule;
context: FeatureFlagRuleUpdatedContext;
}

export interface FeatureFlagsFlagRuleUpdatedEventResponse extends EventResponseBase {
event: 'feature_flags.flag_rule.updated';
data: FlagRuleResponse;
context: FeatureFlagRuleUpdatedContextResponse;
}

export interface FeatureFlagsFlagRuleDeletedEvent extends EventBase {
event: 'feature_flags.flag_rule.deleted';
data: FlagRule;
context: FeatureFlagRuleContext;
}

export interface FeatureFlagsFlagRuleDeletedEventResponse extends EventResponseBase {
event: 'feature_flags.flag_rule.deleted';
data: FlagRuleResponse;
context: FeatureFlagRuleContextResponse;
}

export interface FeatureFlagsFlagTargetCreatedEvent extends EventBase {
event: 'feature_flags.flag_target.created';
data: FlagTargetResource;
context: FeatureFlagContext;
}

export interface FeatureFlagsFlagTargetCreatedEventResponse extends EventResponseBase {
event: 'feature_flags.flag_target.created';
data: FlagTargetResourceResponse;
context: FeatureFlagContextResponse;
}

export interface FeatureFlagsFlagTargetDeletedEvent extends EventBase {
event: 'feature_flags.flag_target.deleted';
data: FlagTargetResource;
context: FeatureFlagContext;
}

export interface FeatureFlagsFlagTargetDeletedEventResponse extends EventResponseBase {
event: 'feature_flags.flag_target.deleted';
data: FlagTargetResourceResponse;
context: FeatureFlagContextResponse;
}

export interface FlagCreatedEvent extends EventBase {
event: 'flag.created';
data: FeatureFlag;
Expand Down Expand Up @@ -1079,6 +1249,14 @@ export type Event =
| OrganizationDomainDeletedEvent
| ApiKeyCreatedEvent
| ApiKeyRevokedEvent
| FeatureFlagsFlagCreatedEvent
| FeatureFlagsFlagUpdatedEvent
| FeatureFlagsFlagDeletedEvent
| FeatureFlagsFlagRuleCreatedEvent
| FeatureFlagsFlagRuleUpdatedEvent
| FeatureFlagsFlagRuleDeletedEvent
| FeatureFlagsFlagTargetCreatedEvent
| FeatureFlagsFlagTargetDeletedEvent
| FlagCreatedEvent
| FlagUpdatedEvent
| FlagDeletedEvent
Expand Down Expand Up @@ -1165,6 +1343,14 @@ export type EventResponse =
| OrganizationDomainDeletedEventResponse
| ApiKeyCreatedEventResponse
| ApiKeyRevokedEventResponse
| FeatureFlagsFlagCreatedEventResponse
| FeatureFlagsFlagUpdatedEventResponse
| FeatureFlagsFlagDeletedEventResponse
| FeatureFlagsFlagRuleCreatedEventResponse
| FeatureFlagsFlagRuleUpdatedEventResponse
| FeatureFlagsFlagRuleDeletedEventResponse
| FeatureFlagsFlagTargetCreatedEventResponse
| FeatureFlagsFlagTargetDeletedEventResponse
| FlagCreatedEventResponse
| FlagUpdatedEventResponse
| FlagDeletedEventResponse
Expand Down
54 changes: 54 additions & 0 deletions src/common/serializers/event.serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ import { deserializeAuthenticationRadarRiskDetectedEvent } from '../../user-mana
import { deserializeApiKey } from '../../api-keys/serializers/api-key.serializer';
import { deserializeOrganizationRoleEvent } from '../../authorization/serializers/organization-role.serializer';
import { deserializePermission } from '../../authorization/serializers/permission.serializer';
import {
deserializeFlagRule,
deserializeFlagTargetResource,
} from '../../feature-flags/serializers';
import {
deserializeFeatureFlagEventData,
deserializeFeatureFlagRuleContext,
deserializeFeatureFlagRuleUpdatedContext,
deserializeFeatureFlagUpdatedContext,
} from './feature-flag-event.serializer';
import { deserializeFeatureFlag } from '../../feature-flags/serializers/feature-flag.serializer';
import { deserializeGroup } from '../../groups/serializers';
import {
Expand Down Expand Up @@ -272,6 +282,50 @@ export const deserializeEvent = (event: EventResponse): Event => {
event: event.event,
data: deserializeApiKey(event.data),
};
case 'feature_flags.flag.created':
case 'feature_flags.flag.deleted':
return {
...eventBase,
event: event.event,
data: deserializeFeatureFlagEventData(event.data),
context: {
clientId: event.context.client_id,
actor: event.context.actor,
},
};
case 'feature_flags.flag.updated':
return {
...eventBase,
event: event.event,
data: deserializeFeatureFlagEventData(event.data),
context: deserializeFeatureFlagUpdatedContext(event.context),
};
case 'feature_flags.flag_rule.created':
case 'feature_flags.flag_rule.deleted':
return {
...eventBase,
event: event.event,
data: deserializeFlagRule(event.data),
context: deserializeFeatureFlagRuleContext(event.context),
};
case 'feature_flags.flag_rule.updated':
return {
...eventBase,
event: event.event,
data: deserializeFlagRule(event.data),
context: deserializeFeatureFlagRuleUpdatedContext(event.context),
};
case 'feature_flags.flag_target.created':
case 'feature_flags.flag_target.deleted':
return {
...eventBase,
event: event.event,
data: deserializeFlagTargetResource(event.data),
context: {
clientId: event.context.client_id,
actor: event.context.actor,
},
};
case 'flag.created':
case 'flag.updated':
case 'flag.deleted':
Expand Down
Loading
Loading