@keystrokehq/segment 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +232 -0
- package/dist/_official/index.d.mts +2 -0
- package/dist/_official/index.mjs +3 -0
- package/dist/_runtime/index.d.mts +1 -0
- package/dist/_runtime/index.mjs +1 -0
- package/dist/audiences.d.mts +282 -0
- package/dist/audiences.mjs +205 -0
- package/dist/client.d.mts +90 -0
- package/dist/client.mjs +337 -0
- package/dist/common-CdGiJbjq.mjs +56 -0
- package/dist/computed-traits.d.mts +215 -0
- package/dist/computed-traits.mjs +149 -0
- package/dist/connection.d.mts +2 -0
- package/dist/connection.mjs +3 -0
- package/dist/crud-SWa_79Hg.mjs +140 -0
- package/dist/deletion-suppression.d.mts +191 -0
- package/dist/deletion-suppression.mjs +103 -0
- package/dist/delivery-overview.d.mts +79 -0
- package/dist/delivery-overview.mjs +54 -0
- package/dist/destination-filters.d.mts +190 -0
- package/dist/destination-filters.mjs +115 -0
- package/dist/destinations.d.mts +473 -0
- package/dist/destinations.mjs +257 -0
- package/dist/edge-functions.d.mts +168 -0
- package/dist/edge-functions.mjs +72 -0
- package/dist/errors-4FGnrowW.mjs +27 -0
- package/dist/events-catalog.d.mts +111 -0
- package/dist/events-catalog.mjs +65 -0
- package/dist/events.d.mts +167 -0
- package/dist/events.mjs +134 -0
- package/dist/factory-CvfKfzMq.mjs +8 -0
- package/dist/functions.d.mts +347 -0
- package/dist/functions.mjs +180 -0
- package/dist/iam-groups.d.mts +284 -0
- package/dist/iam-groups.mjs +144 -0
- package/dist/iam-users.d.mts +205 -0
- package/dist/iam-users.mjs +91 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +1 -0
- package/dist/integration-B5zYasBZ.mjs +25 -0
- package/dist/integration-D2yRaKFR.d.mts +67 -0
- package/dist/labels.d.mts +100 -0
- package/dist/labels.mjs +58 -0
- package/dist/monitoring.d.mts +182 -0
- package/dist/monitoring.mjs +88 -0
- package/dist/profiles-sync.d.mts +127 -0
- package/dist/profiles-sync.mjs +89 -0
- package/dist/profiles.d.mts +197 -0
- package/dist/profiles.mjs +137 -0
- package/dist/reverse-etl.d.mts +448 -0
- package/dist/reverse-etl.mjs +228 -0
- package/dist/roles.d.mts +48 -0
- package/dist/roles.mjs +25 -0
- package/dist/schemas/index.d.mts +57 -0
- package/dist/schemas/index.mjs +3 -0
- package/dist/sources.d.mts +560 -0
- package/dist/sources.mjs +259 -0
- package/dist/tracking-plans.d.mts +351 -0
- package/dist/tracking-plans.mjs +160 -0
- package/dist/tracking.d.mts +499 -0
- package/dist/tracking.mjs +255 -0
- package/dist/transformations.d.mts +188 -0
- package/dist/transformations.mjs +83 -0
- package/dist/triggers.d.mts +54 -0
- package/dist/triggers.mjs +341 -0
- package/dist/usage.d.mts +70 -0
- package/dist/usage.mjs +55 -0
- package/dist/verification.d.mts +17 -0
- package/dist/verification.mjs +51 -0
- package/dist/warehouses.d.mts +341 -0
- package/dist/warehouses.mjs +176 -0
- package/dist/workspaces.d.mts +95 -0
- package/dist/workspaces.mjs +67 -0
- package/package.json +188 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createSegmentClient } from "./client.mjs";
|
|
2
|
+
import { a as segmentLooseObjectSchema, n as segmentIdSchema } from "./common-CdGiJbjq.mjs";
|
|
3
|
+
import { t as segmentOperation } from "./factory-CvfKfzMq.mjs";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
//#region src/audiences.ts
|
|
7
|
+
/**
|
|
8
|
+
* segment/audiences.ts — Engage audiences (space-scoped). 8 actions total
|
|
9
|
+
* (PLAN.md § 6.7).
|
|
10
|
+
*/
|
|
11
|
+
const audienceSchema = segmentLooseObjectSchema({
|
|
12
|
+
id: segmentIdSchema,
|
|
13
|
+
key: z.string().optional(),
|
|
14
|
+
name: z.string().optional(),
|
|
15
|
+
description: z.string().optional(),
|
|
16
|
+
definition: z.record(z.string(), z.unknown()).optional(),
|
|
17
|
+
enabled: z.boolean().optional()
|
|
18
|
+
});
|
|
19
|
+
const membershipSchema = segmentLooseObjectSchema({
|
|
20
|
+
profileId: z.string().optional(),
|
|
21
|
+
isMember: z.boolean().optional(),
|
|
22
|
+
enteredAt: z.string().optional(),
|
|
23
|
+
exitedAt: z.string().optional()
|
|
24
|
+
});
|
|
25
|
+
const memberSchema = segmentLooseObjectSchema({
|
|
26
|
+
profileId: z.string(),
|
|
27
|
+
traits: z.record(z.string(), z.unknown()).optional(),
|
|
28
|
+
enteredAt: z.string().optional()
|
|
29
|
+
});
|
|
30
|
+
const previewResultSchema = segmentLooseObjectSchema({
|
|
31
|
+
count: z.number().optional(),
|
|
32
|
+
sample: z.array(z.record(z.string(), z.unknown())).optional()
|
|
33
|
+
});
|
|
34
|
+
const listAudiences = segmentOperation({
|
|
35
|
+
id: "segment.audiences.list",
|
|
36
|
+
name: "List audiences",
|
|
37
|
+
description: "List audiences defined in an Engage space.",
|
|
38
|
+
input: z.object({
|
|
39
|
+
spaceId: segmentIdSchema,
|
|
40
|
+
pageSize: z.number().int().positive().max(200).optional(),
|
|
41
|
+
pageToken: z.string().optional()
|
|
42
|
+
}),
|
|
43
|
+
output: z.object({
|
|
44
|
+
items: z.array(audienceSchema),
|
|
45
|
+
nextPageToken: z.string().optional()
|
|
46
|
+
}),
|
|
47
|
+
run: async (input, credentials) => {
|
|
48
|
+
const result = await createSegmentClient(credentials).publicApi.list(`/spaces/${encodeURIComponent(input.spaceId)}/audiences`, {
|
|
49
|
+
itemsKey: "audiences",
|
|
50
|
+
items: audienceSchema
|
|
51
|
+
}, { query: {
|
|
52
|
+
count: input.pageSize,
|
|
53
|
+
cursor: input.pageToken
|
|
54
|
+
} });
|
|
55
|
+
return {
|
|
56
|
+
items: [...result.items],
|
|
57
|
+
...result.nextPageToken !== void 0 ? { nextPageToken: result.nextPageToken } : {}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const getAudience = segmentOperation({
|
|
62
|
+
id: "segment.audiences.get",
|
|
63
|
+
name: "Get audience",
|
|
64
|
+
description: "Fetch an audience in an Engage space.",
|
|
65
|
+
input: z.object({
|
|
66
|
+
spaceId: segmentIdSchema,
|
|
67
|
+
id: segmentIdSchema
|
|
68
|
+
}),
|
|
69
|
+
output: audienceSchema,
|
|
70
|
+
run: async (input, credentials) => {
|
|
71
|
+
const body = await createSegmentClient(credentials).publicApi.request(`/spaces/${encodeURIComponent(input.spaceId)}/audiences/${encodeURIComponent(input.id)}`);
|
|
72
|
+
const rec = body;
|
|
73
|
+
return audienceSchema.parse(rec.data?.audience ?? body);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const createAudience = segmentOperation({
|
|
77
|
+
id: "segment.audiences.create",
|
|
78
|
+
name: "Create audience",
|
|
79
|
+
description: "Create a new audience.",
|
|
80
|
+
input: z.object({
|
|
81
|
+
spaceId: segmentIdSchema,
|
|
82
|
+
name: z.string().min(1),
|
|
83
|
+
description: z.string().optional(),
|
|
84
|
+
definition: z.record(z.string(), z.unknown()),
|
|
85
|
+
enabled: z.boolean().optional()
|
|
86
|
+
}),
|
|
87
|
+
output: audienceSchema,
|
|
88
|
+
needsApproval: true,
|
|
89
|
+
run: async (input, credentials) => {
|
|
90
|
+
const client = createSegmentClient(credentials);
|
|
91
|
+
const { spaceId, ...body } = input;
|
|
92
|
+
const response = await client.publicApi.request(`/spaces/${encodeURIComponent(spaceId)}/audiences`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
body
|
|
95
|
+
});
|
|
96
|
+
const rec = response;
|
|
97
|
+
return audienceSchema.parse(rec.data?.audience ?? response);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
const updateAudience = segmentOperation({
|
|
101
|
+
id: "segment.audiences.update",
|
|
102
|
+
name: "Update audience",
|
|
103
|
+
description: "Patch an audience definition.",
|
|
104
|
+
input: z.object({
|
|
105
|
+
spaceId: segmentIdSchema,
|
|
106
|
+
id: segmentIdSchema,
|
|
107
|
+
name: z.string().optional(),
|
|
108
|
+
description: z.string().optional(),
|
|
109
|
+
definition: z.record(z.string(), z.unknown()).optional(),
|
|
110
|
+
enabled: z.boolean().optional()
|
|
111
|
+
}),
|
|
112
|
+
output: audienceSchema,
|
|
113
|
+
needsApproval: true,
|
|
114
|
+
run: async (input, credentials) => {
|
|
115
|
+
const client = createSegmentClient(credentials);
|
|
116
|
+
const { spaceId, id, ...body } = input;
|
|
117
|
+
const response = await client.publicApi.request(`/spaces/${encodeURIComponent(spaceId)}/audiences/${encodeURIComponent(id)}`, {
|
|
118
|
+
method: "PATCH",
|
|
119
|
+
body
|
|
120
|
+
});
|
|
121
|
+
const rec = response;
|
|
122
|
+
return audienceSchema.parse(rec.data?.audience ?? response);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const deleteAudience = segmentOperation({
|
|
126
|
+
id: "segment.audiences.delete",
|
|
127
|
+
name: "Delete audience",
|
|
128
|
+
description: "Delete an audience.",
|
|
129
|
+
input: z.object({
|
|
130
|
+
spaceId: segmentIdSchema,
|
|
131
|
+
id: segmentIdSchema
|
|
132
|
+
}),
|
|
133
|
+
output: z.object({ deleted: z.literal(true) }),
|
|
134
|
+
needsApproval: true,
|
|
135
|
+
run: async (input, credentials) => {
|
|
136
|
+
await createSegmentClient(credentials).publicApi.request(`/spaces/${encodeURIComponent(input.spaceId)}/audiences/${encodeURIComponent(input.id)}`, { method: "DELETE" });
|
|
137
|
+
return { deleted: true };
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const previewAudience = segmentOperation({
|
|
141
|
+
id: "segment.audiences.preview",
|
|
142
|
+
name: "Preview audience",
|
|
143
|
+
description: "Run a preview compute over an audience definition without saving.",
|
|
144
|
+
input: z.object({
|
|
145
|
+
spaceId: segmentIdSchema,
|
|
146
|
+
definition: z.record(z.string(), z.unknown())
|
|
147
|
+
}),
|
|
148
|
+
output: previewResultSchema,
|
|
149
|
+
needsApproval: true,
|
|
150
|
+
run: async (input, credentials) => {
|
|
151
|
+
const body = await createSegmentClient(credentials).publicApi.request(`/spaces/${encodeURIComponent(input.spaceId)}/audiences/previews`, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: { definition: input.definition }
|
|
154
|
+
});
|
|
155
|
+
const rec = body;
|
|
156
|
+
return previewResultSchema.parse(rec.data?.preview ?? body);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const getAudienceMembership = segmentOperation({
|
|
160
|
+
id: "segment.audiences.getMembership",
|
|
161
|
+
name: "Get audience membership",
|
|
162
|
+
description: "Check whether a specific profile is a member of an audience.",
|
|
163
|
+
input: z.object({
|
|
164
|
+
spaceId: segmentIdSchema,
|
|
165
|
+
id: segmentIdSchema,
|
|
166
|
+
profileId: segmentIdSchema
|
|
167
|
+
}),
|
|
168
|
+
output: membershipSchema,
|
|
169
|
+
run: async (input, credentials) => {
|
|
170
|
+
const body = await createSegmentClient(credentials).publicApi.request(`/spaces/${encodeURIComponent(input.spaceId)}/audiences/${encodeURIComponent(input.id)}/membership/${encodeURIComponent(input.profileId)}`);
|
|
171
|
+
const rec = body;
|
|
172
|
+
return membershipSchema.parse(rec.data ?? body);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
const listAudienceMembers = segmentOperation({
|
|
176
|
+
id: "segment.audiences.listMembers",
|
|
177
|
+
name: "List audience members",
|
|
178
|
+
description: "List the profiles currently in an audience.",
|
|
179
|
+
input: z.object({
|
|
180
|
+
spaceId: segmentIdSchema,
|
|
181
|
+
id: segmentIdSchema,
|
|
182
|
+
pageSize: z.number().int().positive().max(200).optional(),
|
|
183
|
+
pageToken: z.string().optional()
|
|
184
|
+
}),
|
|
185
|
+
output: z.object({
|
|
186
|
+
items: z.array(memberSchema),
|
|
187
|
+
nextPageToken: z.string().optional()
|
|
188
|
+
}),
|
|
189
|
+
run: async (input, credentials) => {
|
|
190
|
+
const result = await createSegmentClient(credentials).publicApi.list(`/spaces/${encodeURIComponent(input.spaceId)}/audiences/${encodeURIComponent(input.id)}/members`, {
|
|
191
|
+
itemsKey: "members",
|
|
192
|
+
items: memberSchema
|
|
193
|
+
}, { query: {
|
|
194
|
+
count: input.pageSize,
|
|
195
|
+
cursor: input.pageToken
|
|
196
|
+
} });
|
|
197
|
+
return {
|
|
198
|
+
items: [...result.items],
|
|
199
|
+
...result.nextPageToken !== void 0 ? { nextPageToken: result.nextPageToken } : {}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
//#endregion
|
|
205
|
+
export { audienceSchema, createAudience, deleteAudience, getAudience, getAudienceMembership, listAudienceMembers, listAudiences, previewAudience, updateAudience };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { n as SegmentRegion, t as SegmentCredentials } from "./integration-D2yRaKFR.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/client.d.ts
|
|
5
|
+
declare const PUBLIC_API_HOSTS: Readonly<Record<SegmentRegion, string>>;
|
|
6
|
+
declare const TRACKING_API_HOSTS: Readonly<Record<SegmentRegion, string>>;
|
|
7
|
+
declare const PROFILE_API_HOST = "https://profiles.segment.com/v1";
|
|
8
|
+
/** Segment-enforced hard limit for a single event body (Track/Identify/…). */
|
|
9
|
+
declare const MAX_EVENT_BYTES: number;
|
|
10
|
+
/** Segment-enforced hard limit for a `/v1/batch` envelope. */
|
|
11
|
+
declare const MAX_BATCH_BYTES: number;
|
|
12
|
+
type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
13
|
+
interface CreateSegmentClientOptions {
|
|
14
|
+
/** Override the global `fetch` (for tests, custom timeouts, etc.). */
|
|
15
|
+
readonly fetch?: FetchLike;
|
|
16
|
+
/** Maximum number of retries on 429/5xx. Defaults to 3. */
|
|
17
|
+
readonly maxRetries?: number;
|
|
18
|
+
/** Maximum overall backoff between retries in ms. Defaults to 10_000. */
|
|
19
|
+
readonly maxBackoffMs?: number;
|
|
20
|
+
/** Sleep function (override for tests). */
|
|
21
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
type QueryPrimitive = string | number | boolean;
|
|
24
|
+
type QueryValue = QueryPrimitive | readonly QueryPrimitive[] | undefined;
|
|
25
|
+
interface PublicRequestOptions {
|
|
26
|
+
readonly method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
27
|
+
readonly query?: Readonly<Record<string, QueryValue>>;
|
|
28
|
+
readonly body?: unknown;
|
|
29
|
+
readonly timeoutMs?: number;
|
|
30
|
+
}
|
|
31
|
+
interface ListOptions<TOut> {
|
|
32
|
+
readonly itemsKey: string;
|
|
33
|
+
readonly items: z.ZodType<TOut>;
|
|
34
|
+
}
|
|
35
|
+
interface ListResult<T> {
|
|
36
|
+
readonly items: readonly T[];
|
|
37
|
+
readonly nextPageToken?: string;
|
|
38
|
+
readonly totalEntries?: number;
|
|
39
|
+
}
|
|
40
|
+
interface BatchSplitChunk<T> {
|
|
41
|
+
readonly events: readonly T[];
|
|
42
|
+
readonly bytes: number;
|
|
43
|
+
}
|
|
44
|
+
interface BatchSplitResult<T> {
|
|
45
|
+
readonly chunks: readonly BatchSplitChunk<T>[];
|
|
46
|
+
readonly rejected: readonly {
|
|
47
|
+
readonly event: T;
|
|
48
|
+
readonly bytes: number;
|
|
49
|
+
}[];
|
|
50
|
+
}
|
|
51
|
+
declare function splitBatch<T>(events: readonly T[], options?: {
|
|
52
|
+
readonly maxBatchBytes?: number;
|
|
53
|
+
readonly maxEventBytes?: number;
|
|
54
|
+
}): BatchSplitResult<T>;
|
|
55
|
+
interface PublicApiClient {
|
|
56
|
+
readonly baseUrl: string;
|
|
57
|
+
readonly request: <T>(path: string, options?: PublicRequestOptions) => Promise<T>;
|
|
58
|
+
readonly list: <T>(path: string, listOpts: ListOptions<T>, requestOpts?: PublicRequestOptions) => Promise<ListResult<T>>;
|
|
59
|
+
}
|
|
60
|
+
interface TrackingApiClient {
|
|
61
|
+
readonly baseUrl: string;
|
|
62
|
+
/** Send a single tracking event. `method` defaults to the endpoint path
|
|
63
|
+
* (e.g. `identify`, `track`). `sourceId` selects which write key to use. */
|
|
64
|
+
readonly send: (method: TrackingMethod, payload: Record<string, unknown>, options?: {
|
|
65
|
+
readonly sourceId?: string;
|
|
66
|
+
}) => Promise<void>;
|
|
67
|
+
readonly batch: (events: readonly Record<string, unknown>[], options?: {
|
|
68
|
+
readonly sourceId?: string;
|
|
69
|
+
}) => Promise<{
|
|
70
|
+
readonly chunksSent: number;
|
|
71
|
+
readonly rejected: readonly Record<string, unknown>[];
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
interface ProfileApiClient {
|
|
75
|
+
readonly baseUrl: string;
|
|
76
|
+
readonly request: <T>(path: string, options?: PublicRequestOptions & {
|
|
77
|
+
readonly spaceId?: string;
|
|
78
|
+
}) => Promise<T>;
|
|
79
|
+
}
|
|
80
|
+
type TrackingMethod = 'identify' | 'track' | 'page' | 'screen' | 'group' | 'alias' | 'batch';
|
|
81
|
+
interface SegmentClient {
|
|
82
|
+
readonly credentials: SegmentCredentials;
|
|
83
|
+
readonly region: SegmentRegion;
|
|
84
|
+
readonly publicApi: PublicApiClient;
|
|
85
|
+
readonly tracking: TrackingApiClient;
|
|
86
|
+
readonly profiles: ProfileApiClient;
|
|
87
|
+
}
|
|
88
|
+
declare function createSegmentClient(credentials: SegmentCredentials, options?: CreateSegmentClientOptions): SegmentClient;
|
|
89
|
+
//#endregion
|
|
90
|
+
export { BatchSplitChunk, BatchSplitResult, CreateSegmentClientOptions, FetchLike, ListOptions, ListResult, MAX_BATCH_BYTES, MAX_EVENT_BYTES, PROFILE_API_HOST, PUBLIC_API_HOSTS, ProfileApiClient, PublicApiClient, PublicRequestOptions, SegmentClient, TRACKING_API_HOSTS, TrackingApiClient, TrackingMethod, createSegmentClient, splitBatch };
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { t as SegmentApiError } from "./errors-4FGnrowW.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/client.ts
|
|
5
|
+
/**
|
|
6
|
+
* segment/client.ts
|
|
7
|
+
*
|
|
8
|
+
* Typed `fetch`-based client for all three Segment authentication surfaces:
|
|
9
|
+
*
|
|
10
|
+
* - **Public API** (`api.segmentapis.com` US / `eu1.api.segmentapis.com`
|
|
11
|
+
* EU) — workspace-scoped Bearer token. Cursor pagination, retry on 429
|
|
12
|
+
* (respects `Retry-After`) and 5xx.
|
|
13
|
+
* - **Tracking API** (`api.segment.io` US / `events.eu1.segmentapis.com`
|
|
14
|
+
* EU) — HTTP Basic with per-source write key as username; no password.
|
|
15
|
+
* Enforces 500 KB per batch and 32 KB per event client-side and exposes a
|
|
16
|
+
* `splitBatch` helper for chunking.
|
|
17
|
+
* - **Profile API** (`profiles.segment.com`) — HTTP Basic with per-space
|
|
18
|
+
* access token as username.
|
|
19
|
+
*
|
|
20
|
+
* The official `@segment/public-api-sdk-typescript` package was considered and
|
|
21
|
+
* rejected (see `IMPLEMENTATION_NOTES.md` § 3): it depends on the deprecated
|
|
22
|
+
* `request` library and `bluebird`, neither of which is ESM-friendly or
|
|
23
|
+
* usable in Cloudflare Workers / V8 isolates (our executor tier).
|
|
24
|
+
*/
|
|
25
|
+
const PUBLIC_API_HOSTS = {
|
|
26
|
+
us: "https://api.segmentapis.com",
|
|
27
|
+
eu: "https://eu1.api.segmentapis.com"
|
|
28
|
+
};
|
|
29
|
+
const TRACKING_API_HOSTS = {
|
|
30
|
+
us: "https://api.segment.io/v1",
|
|
31
|
+
eu: "https://events.eu1.segmentapis.com/v1"
|
|
32
|
+
};
|
|
33
|
+
const PROFILE_API_HOST = "https://profiles.segment.com/v1";
|
|
34
|
+
/** Segment-enforced hard limit for a single event body (Track/Identify/…). */
|
|
35
|
+
const MAX_EVENT_BYTES = 32 * 1024;
|
|
36
|
+
/** Segment-enforced hard limit for a `/v1/batch` envelope. */
|
|
37
|
+
const MAX_BATCH_BYTES = 500 * 1024;
|
|
38
|
+
function splitBatch(events, options) {
|
|
39
|
+
const maxBatch = options?.maxBatchBytes ?? MAX_BATCH_BYTES;
|
|
40
|
+
const maxEvent = options?.maxEventBytes ?? MAX_EVENT_BYTES;
|
|
41
|
+
const chunks = [];
|
|
42
|
+
const rejected = [];
|
|
43
|
+
let current = [];
|
|
44
|
+
let currentBytes = envelopeBytes(0);
|
|
45
|
+
for (const event of events) {
|
|
46
|
+
const bytes = jsonByteLength(event);
|
|
47
|
+
if (bytes > maxEvent) {
|
|
48
|
+
rejected.push({
|
|
49
|
+
event,
|
|
50
|
+
bytes
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const addition = bytes + 1;
|
|
55
|
+
if (currentBytes + addition > maxBatch && current.length > 0) {
|
|
56
|
+
chunks.push({
|
|
57
|
+
events: current,
|
|
58
|
+
bytes: currentBytes
|
|
59
|
+
});
|
|
60
|
+
current = [];
|
|
61
|
+
currentBytes = envelopeBytes(0);
|
|
62
|
+
}
|
|
63
|
+
current.push(event);
|
|
64
|
+
currentBytes += addition;
|
|
65
|
+
}
|
|
66
|
+
if (current.length > 0) chunks.push({
|
|
67
|
+
events: current,
|
|
68
|
+
bytes: currentBytes
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
chunks,
|
|
72
|
+
rejected
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function envelopeBytes(eventsBytes) {
|
|
76
|
+
return 12 + eventsBytes;
|
|
77
|
+
}
|
|
78
|
+
function jsonByteLength(value) {
|
|
79
|
+
return new TextEncoder().encode(JSON.stringify(value ?? null)).length;
|
|
80
|
+
}
|
|
81
|
+
const writeKeyMapSchema = z.record(z.string().min(1), z.string().min(1));
|
|
82
|
+
function resolveWriteKey(credentials, context) {
|
|
83
|
+
const map = parseWriteKeyMap(credentials.SEGMENT_WRITE_KEYS_JSON);
|
|
84
|
+
if (context.sourceId && map?.[context.sourceId]) return map[context.sourceId];
|
|
85
|
+
const fallback = credentials.SEGMENT_DEFAULT_WRITE_KEY;
|
|
86
|
+
if (fallback) return fallback;
|
|
87
|
+
throw new SegmentApiError({
|
|
88
|
+
kind: "missing_credential",
|
|
89
|
+
message: context.sourceId ? `No Segment write key is configured for source "${context.sourceId}". Provide it in SEGMENT_WRITE_KEYS_JSON or set SEGMENT_DEFAULT_WRITE_KEY.` : "No Segment write key is configured. Pass a sourceId resolvable from SEGMENT_WRITE_KEYS_JSON, or set SEGMENT_DEFAULT_WRITE_KEY."
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function parseWriteKeyMap(raw) {
|
|
93
|
+
if (!raw) return void 0;
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(raw);
|
|
97
|
+
} catch (cause) {
|
|
98
|
+
throw new SegmentApiError({
|
|
99
|
+
kind: "validation",
|
|
100
|
+
message: "SEGMENT_WRITE_KEYS_JSON is not valid JSON.",
|
|
101
|
+
cause
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const result = writeKeyMapSchema.safeParse(parsed);
|
|
105
|
+
if (!result.success) throw new SegmentApiError({
|
|
106
|
+
kind: "validation",
|
|
107
|
+
message: "SEGMENT_WRITE_KEYS_JSON must be a JSON object of { sourceId: writeKey }.",
|
|
108
|
+
cause: result.error
|
|
109
|
+
});
|
|
110
|
+
return result.data;
|
|
111
|
+
}
|
|
112
|
+
async function runRequest(opts, runtime) {
|
|
113
|
+
const url = new URL(`${opts.baseUrl}${opts.path}`);
|
|
114
|
+
if (opts.query) appendQuery(url, opts.query);
|
|
115
|
+
const init = {
|
|
116
|
+
method: opts.method ?? "GET",
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: opts.authHeader,
|
|
119
|
+
Accept: opts.acceptHeader ?? "application/json",
|
|
120
|
+
...opts.body !== void 0 ? { "Content-Type": opts.contentType ?? "application/json" } : {}
|
|
121
|
+
},
|
|
122
|
+
...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
|
|
123
|
+
};
|
|
124
|
+
for (let attempt = 0;; attempt += 1) {
|
|
125
|
+
let response;
|
|
126
|
+
try {
|
|
127
|
+
response = await runtime.fetch(url, init);
|
|
128
|
+
} catch (cause) {
|
|
129
|
+
if (attempt < runtime.maxRetries) {
|
|
130
|
+
await runtime.sleep(computeBackoff(attempt, runtime.maxBackoffMs));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
throw new SegmentApiError({
|
|
134
|
+
kind: "network",
|
|
135
|
+
message: `Segment request failed (${opts.method ?? "GET"} ${opts.path}): ${stringifyError(cause)}`,
|
|
136
|
+
cause
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (response.status === 204) return;
|
|
140
|
+
const body = (response.headers.get("content-type") ?? "").includes("application/json") ? await response.json().catch(() => void 0) : await response.text().catch(() => "");
|
|
141
|
+
if (response.ok) return body;
|
|
142
|
+
if (response.status === 429 && attempt < runtime.maxRetries) {
|
|
143
|
+
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
144
|
+
await runtime.sleep(retryAfter ?? computeBackoff(attempt, runtime.maxBackoffMs));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (response.status >= 500 && response.status < 600 && attempt < runtime.maxRetries) {
|
|
148
|
+
await runtime.sleep(computeBackoff(attempt, runtime.maxBackoffMs));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
throw toSegmentError(response, body, opts);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function toSegmentError(response, body, opts) {
|
|
155
|
+
const kind = response.status === 401 || response.status === 403 ? "auth" : response.status === 413 ? "batch_too_large" : response.status === 429 ? "rate_limit" : "http";
|
|
156
|
+
const message = extractMessage(body) ?? `${opts.method ?? "GET"} ${opts.path} -> ${response.status}`;
|
|
157
|
+
const code = extractCode(body);
|
|
158
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
159
|
+
return new SegmentApiError({
|
|
160
|
+
kind,
|
|
161
|
+
status: response.status,
|
|
162
|
+
code,
|
|
163
|
+
requestId,
|
|
164
|
+
body,
|
|
165
|
+
message: `Segment API ${opts.method ?? "GET"} ${opts.path} failed (${response.status}): ${message}`
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function extractMessage(body) {
|
|
169
|
+
if (!body || typeof body !== "object") return typeof body === "string" ? body : void 0;
|
|
170
|
+
const record = body;
|
|
171
|
+
if (typeof record.message === "string") return record.message;
|
|
172
|
+
if (typeof record.error === "string") return record.error;
|
|
173
|
+
}
|
|
174
|
+
function extractCode(body) {
|
|
175
|
+
if (!body || typeof body !== "object") return void 0;
|
|
176
|
+
const record = body;
|
|
177
|
+
if (typeof record.code === "string") return record.code;
|
|
178
|
+
if (typeof record.errorCode === "string") return record.errorCode;
|
|
179
|
+
}
|
|
180
|
+
function parseRetryAfter(value) {
|
|
181
|
+
if (!value) return void 0;
|
|
182
|
+
const seconds = Number(value);
|
|
183
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
184
|
+
const dateMs = Date.parse(value);
|
|
185
|
+
if (!Number.isNaN(dateMs)) {
|
|
186
|
+
const delta = dateMs - Date.now();
|
|
187
|
+
return delta > 0 ? delta : 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function computeBackoff(attempt, maxBackoffMs) {
|
|
191
|
+
const base = Math.min(maxBackoffMs, 500 * 2 ** attempt);
|
|
192
|
+
const jitter = Math.random() * base * .25;
|
|
193
|
+
return Math.floor(base + jitter);
|
|
194
|
+
}
|
|
195
|
+
function stringifyError(err) {
|
|
196
|
+
if (err instanceof Error) return err.message;
|
|
197
|
+
return String(err);
|
|
198
|
+
}
|
|
199
|
+
function appendQuery(url, query) {
|
|
200
|
+
for (const [key, value] of Object.entries(query)) {
|
|
201
|
+
if (value === void 0) continue;
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
for (const entry of value) url.searchParams.append(key, String(entry));
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
url.searchParams.set(key, String(value));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function basicAuthHeader(usernameWithNoPassword) {
|
|
210
|
+
return `Basic ${typeof globalThis.btoa === "function" ? globalThis.btoa(`${usernameWithNoPassword}:`) : Buffer.from(`${usernameWithNoPassword}:`, "utf8").toString("base64")}`;
|
|
211
|
+
}
|
|
212
|
+
function createSegmentClient(credentials, options = {}) {
|
|
213
|
+
const region = credentials.SEGMENT_REGION ?? "us";
|
|
214
|
+
const runtime = {
|
|
215
|
+
fetch: options.fetch ?? globalThis.fetch.bind(globalThis),
|
|
216
|
+
maxRetries: options.maxRetries ?? 3,
|
|
217
|
+
maxBackoffMs: options.maxBackoffMs ?? 1e4,
|
|
218
|
+
sleep: options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)))
|
|
219
|
+
};
|
|
220
|
+
const publicBase = PUBLIC_API_HOSTS[region];
|
|
221
|
+
const trackingBase = TRACKING_API_HOSTS[region];
|
|
222
|
+
const bearer = `Bearer ${credentials.SEGMENT_PUBLIC_API_TOKEN}`;
|
|
223
|
+
return {
|
|
224
|
+
credentials,
|
|
225
|
+
region,
|
|
226
|
+
publicApi: {
|
|
227
|
+
baseUrl: publicBase,
|
|
228
|
+
request: (path, opts = {}) => runRequest({
|
|
229
|
+
...opts,
|
|
230
|
+
baseUrl: publicBase,
|
|
231
|
+
path,
|
|
232
|
+
authHeader: bearer,
|
|
233
|
+
acceptHeader: "application/vnd.segment.v1+json"
|
|
234
|
+
}, runtime),
|
|
235
|
+
list: async (path, listOpts, requestOpts = {}) => {
|
|
236
|
+
return parseListResult(await runRequest({
|
|
237
|
+
...requestOpts,
|
|
238
|
+
baseUrl: publicBase,
|
|
239
|
+
path,
|
|
240
|
+
authHeader: bearer,
|
|
241
|
+
acceptHeader: "application/vnd.segment.v1+json"
|
|
242
|
+
}, runtime), listOpts);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
tracking: {
|
|
246
|
+
baseUrl: trackingBase,
|
|
247
|
+
send: async (method, payload, opts = {}) => {
|
|
248
|
+
if (method === "batch") throw new SegmentApiError({
|
|
249
|
+
kind: "validation",
|
|
250
|
+
message: "Use client.tracking.batch(...) for batched events, not send(\"batch\", ...)."
|
|
251
|
+
});
|
|
252
|
+
const writeKey = resolveWriteKey(credentials, { sourceId: opts.sourceId });
|
|
253
|
+
const bytes = jsonByteLength(payload);
|
|
254
|
+
if (bytes > MAX_EVENT_BYTES) throw new SegmentApiError({
|
|
255
|
+
kind: "event_too_large",
|
|
256
|
+
message: `Segment tracking event is ${bytes} bytes; limit is ${MAX_EVENT_BYTES} bytes per request.`
|
|
257
|
+
});
|
|
258
|
+
await runRequest({
|
|
259
|
+
baseUrl: trackingBase,
|
|
260
|
+
path: `/${method}`,
|
|
261
|
+
method: "POST",
|
|
262
|
+
body: payload,
|
|
263
|
+
authHeader: basicAuthHeader(writeKey)
|
|
264
|
+
}, runtime);
|
|
265
|
+
},
|
|
266
|
+
batch: async (events, opts = {}) => {
|
|
267
|
+
const writeKey = resolveWriteKey(credentials, { sourceId: opts.sourceId });
|
|
268
|
+
const { chunks, rejected } = splitBatch(events);
|
|
269
|
+
for (const chunk of chunks) await runRequest({
|
|
270
|
+
baseUrl: trackingBase,
|
|
271
|
+
path: "/batch",
|
|
272
|
+
method: "POST",
|
|
273
|
+
body: { batch: chunk.events },
|
|
274
|
+
authHeader: basicAuthHeader(writeKey)
|
|
275
|
+
}, runtime);
|
|
276
|
+
return {
|
|
277
|
+
chunksSent: chunks.length,
|
|
278
|
+
rejected: rejected.map((r) => r.event)
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
profiles: {
|
|
283
|
+
baseUrl: PROFILE_API_HOST,
|
|
284
|
+
request: async (path, opts = {}) => {
|
|
285
|
+
const token = credentials.SEGMENT_PROFILE_API_TOKEN;
|
|
286
|
+
if (!token) throw new SegmentApiError({
|
|
287
|
+
kind: "missing_credential",
|
|
288
|
+
message: "Profile API access requires SEGMENT_PROFILE_API_TOKEN. Generate a per-space access token in Segment Engage settings and add it to the Keystroke connection."
|
|
289
|
+
});
|
|
290
|
+
const spaceId = opts.spaceId ?? credentials.SEGMENT_PROFILE_SPACE_ID;
|
|
291
|
+
if (!spaceId) throw new SegmentApiError({
|
|
292
|
+
kind: "missing_credential",
|
|
293
|
+
message: "Profile API calls require a spaceId — pass one as input, or set SEGMENT_PROFILE_SPACE_ID on the connection."
|
|
294
|
+
});
|
|
295
|
+
return runRequest({
|
|
296
|
+
...opts,
|
|
297
|
+
baseUrl: PROFILE_API_HOST,
|
|
298
|
+
path: path.replace("{spaceId}", encodeURIComponent(spaceId)),
|
|
299
|
+
authHeader: basicAuthHeader(token)
|
|
300
|
+
}, runtime);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function parseListResult(raw, opts) {
|
|
306
|
+
const body = unwrapListBody(raw);
|
|
307
|
+
const container = body.data ?? body;
|
|
308
|
+
const items = extractItems(container, opts.itemsKey).map((entry) => opts.items.parse(entry));
|
|
309
|
+
const pagination = extractPagination(container);
|
|
310
|
+
return {
|
|
311
|
+
items,
|
|
312
|
+
...pagination.next !== void 0 ? { nextPageToken: pagination.next } : {},
|
|
313
|
+
...pagination.totalEntries !== void 0 ? { totalEntries: pagination.totalEntries } : {}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function unwrapListBody(raw) {
|
|
317
|
+
if (!raw || typeof raw !== "object") return {};
|
|
318
|
+
return raw;
|
|
319
|
+
}
|
|
320
|
+
function extractItems(container, key) {
|
|
321
|
+
const value = container[key];
|
|
322
|
+
return Array.isArray(value) ? value : [];
|
|
323
|
+
}
|
|
324
|
+
function extractPagination(container) {
|
|
325
|
+
const pagination = container.pagination;
|
|
326
|
+
if (!pagination || typeof pagination !== "object") return {};
|
|
327
|
+
const record = pagination;
|
|
328
|
+
const next = typeof record.next === "string" ? record.next : void 0;
|
|
329
|
+
const totalEntries = typeof record.totalEntries === "number" ? record.totalEntries : void 0;
|
|
330
|
+
return {
|
|
331
|
+
...next !== void 0 ? { next } : {},
|
|
332
|
+
...totalEntries !== void 0 ? { totalEntries } : {}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
//#endregion
|
|
337
|
+
export { MAX_BATCH_BYTES, MAX_EVENT_BYTES, PROFILE_API_HOST, PUBLIC_API_HOSTS, TRACKING_API_HOSTS, createSegmentClient, splitBatch };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/schemas/common.ts
|
|
4
|
+
/**
|
|
5
|
+
* segment/schemas/common.ts
|
|
6
|
+
*
|
|
7
|
+
* Zod v4 primitives shared across every Segment domain. Each domain file
|
|
8
|
+
* defines its own resource-shape schemas inline (AUTHORING.md § "inline
|
|
9
|
+
* single-use schemas") and reuses these primitives.
|
|
10
|
+
*/
|
|
11
|
+
/** Non-empty trimmed string. */
|
|
12
|
+
const segmentIdSchema = z.string().trim().min(1);
|
|
13
|
+
/** ISO-8601 timestamp as emitted by Segment. Accept either a real ISO-8601
|
|
14
|
+
* datetime or any non-empty string (Segment occasionally returns
|
|
15
|
+
* human-readable dates for some `finishedAt` fields). */
|
|
16
|
+
const segmentTimestampSchema = z.union([z.iso.datetime({ offset: true }), segmentIdSchema]);
|
|
17
|
+
/** Forward-only cursor token the Public API returns on list endpoints under
|
|
18
|
+
* `pagination.next`. */
|
|
19
|
+
const segmentCursorSchema = z.string().min(1);
|
|
20
|
+
/** A permissively-typed object. Used for provider fields we pass through
|
|
21
|
+
* untouched (e.g. destination settings blobs whose shape depends on which
|
|
22
|
+
* destination is configured). Parsed so we never leak unknown values into
|
|
23
|
+
* typed outputs, but we accept any keys. */
|
|
24
|
+
function segmentLooseObjectSchema(shape = {}) {
|
|
25
|
+
return z.object(shape).catchall(z.unknown());
|
|
26
|
+
}
|
|
27
|
+
/** Segment Public API pagination envelope. */
|
|
28
|
+
const segmentPaginationSchema = z.object({
|
|
29
|
+
current: segmentCursorSchema.optional(),
|
|
30
|
+
next: segmentCursorSchema.optional(),
|
|
31
|
+
previous: segmentCursorSchema.optional(),
|
|
32
|
+
totalEntries: z.number().int().nonnegative().optional()
|
|
33
|
+
}).partial();
|
|
34
|
+
/** Common pagination input fields accepted by every list op. */
|
|
35
|
+
const segmentPaginationInputSchema = z.object({
|
|
36
|
+
pageSize: z.number().int().positive().max(200).optional(),
|
|
37
|
+
pageToken: segmentCursorSchema.optional()
|
|
38
|
+
});
|
|
39
|
+
/** Segment label — `key:value`. */
|
|
40
|
+
const segmentLabelSchema = z.object({
|
|
41
|
+
key: segmentIdSchema,
|
|
42
|
+
value: segmentIdSchema,
|
|
43
|
+
description: z.string().optional()
|
|
44
|
+
});
|
|
45
|
+
/** Envelope for a Public API list response. The actual payload key is the
|
|
46
|
+
* resource name (`sources`, `destinations`, …); callers wrap with the
|
|
47
|
+
* resource-specific schema. */
|
|
48
|
+
function segmentListEnvelopeSchema(itemsKey, item) {
|
|
49
|
+
return segmentLooseObjectSchema({
|
|
50
|
+
[itemsKey]: z.array(item),
|
|
51
|
+
pagination: segmentPaginationSchema.optional()
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
export { segmentLooseObjectSchema as a, segmentTimestampSchema as c, segmentListEnvelopeSchema as i, segmentIdSchema as n, segmentPaginationInputSchema as o, segmentLabelSchema as r, segmentPaginationSchema as s, segmentCursorSchema as t };
|