@runbandit/sdk 1.0.0
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 +32 -0
- package/dist/index.d.ts +336 -0
- package/dist/index.js +390 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @runbandit/sdk
|
|
2
|
+
|
|
3
|
+
Browser/Node.js SDK for [Bandit](https://runbandit.com) — algorithmic A/B testing and treatment optimization with multi-armed bandits.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @runbandit/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { BanditsClient } from '@runbandit/sdk'
|
|
15
|
+
|
|
16
|
+
const client = new BanditsClient({
|
|
17
|
+
apiUrl: 'https://app.runbandit.com',
|
|
18
|
+
apiKey: 'your-api-key',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const assignment = await client.getAssignment('experiment-id', 'user-id')
|
|
22
|
+
showTreatment(assignment.treatment.config)
|
|
23
|
+
|
|
24
|
+
// Track a conversion. Pass { immediate: true } for events right before navigation.
|
|
25
|
+
client.trackNamedEvent({
|
|
26
|
+
name: 'conversion',
|
|
27
|
+
value: 29.99,
|
|
28
|
+
metadata: { assignmentId: assignment.assignmentId },
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
See the [SDK docs](https://runbandit.com/docs/sdk) for the full API.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
interface Company {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
subdomain?: string;
|
|
5
|
+
domain?: string;
|
|
6
|
+
plan: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
status: string;
|
|
9
|
+
createdAt: Date;
|
|
10
|
+
updatedAt: Date;
|
|
11
|
+
}
|
|
12
|
+
interface User {
|
|
13
|
+
id: string;
|
|
14
|
+
email: string;
|
|
15
|
+
name: string;
|
|
16
|
+
image?: string;
|
|
17
|
+
role: string;
|
|
18
|
+
companyId: string | null;
|
|
19
|
+
emailVerified?: boolean;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
updatedAt: Date;
|
|
22
|
+
}
|
|
23
|
+
interface Session {
|
|
24
|
+
id: string;
|
|
25
|
+
userId: string;
|
|
26
|
+
token: string;
|
|
27
|
+
expiresAt: Date;
|
|
28
|
+
ipAddress?: string | null;
|
|
29
|
+
userAgent?: string | null;
|
|
30
|
+
createdAt: Date;
|
|
31
|
+
updatedAt: Date;
|
|
32
|
+
}
|
|
33
|
+
interface Account {
|
|
34
|
+
id: string;
|
|
35
|
+
userId: string;
|
|
36
|
+
provider: string;
|
|
37
|
+
providerAccountId: string;
|
|
38
|
+
accessToken?: string;
|
|
39
|
+
refreshToken?: string;
|
|
40
|
+
accessTokenExpiresAt?: Date;
|
|
41
|
+
refreshTokenExpiresAt?: Date;
|
|
42
|
+
scope?: string;
|
|
43
|
+
idToken?: string;
|
|
44
|
+
sessionState?: string;
|
|
45
|
+
createdAt: Date;
|
|
46
|
+
updatedAt: Date;
|
|
47
|
+
}
|
|
48
|
+
interface Experiment {
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
algorithm: BanditAlgorithm;
|
|
53
|
+
status: ExperimentStatus;
|
|
54
|
+
contentType: ContentType;
|
|
55
|
+
treatments: Treatment[];
|
|
56
|
+
context?: Record<string, any>;
|
|
57
|
+
image?: string;
|
|
58
|
+
emoji?: string;
|
|
59
|
+
assignmentTtl: number;
|
|
60
|
+
companyId: string;
|
|
61
|
+
createdAt: Date;
|
|
62
|
+
updatedAt: Date;
|
|
63
|
+
endAt?: Date;
|
|
64
|
+
}
|
|
65
|
+
interface Treatment {
|
|
66
|
+
id: string;
|
|
67
|
+
experimentId: string;
|
|
68
|
+
name: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
config: VariantConfig;
|
|
71
|
+
weight?: number;
|
|
72
|
+
companyId: string;
|
|
73
|
+
createdAt: Date;
|
|
74
|
+
}
|
|
75
|
+
interface AlgorithmResult {
|
|
76
|
+
treatmentId: string;
|
|
77
|
+
confidence: number;
|
|
78
|
+
}
|
|
79
|
+
interface BanditResult {
|
|
80
|
+
assignmentId: string;
|
|
81
|
+
treatmentId: string;
|
|
82
|
+
confidence: number;
|
|
83
|
+
metadata?: Record<string, any>;
|
|
84
|
+
}
|
|
85
|
+
interface ContextualFeatures {
|
|
86
|
+
[key: string]: string | number | boolean;
|
|
87
|
+
}
|
|
88
|
+
declare enum ExperimentStatus {
|
|
89
|
+
DRAFT = "draft",
|
|
90
|
+
ACTIVE = "active",
|
|
91
|
+
PAUSED = "paused",
|
|
92
|
+
COMPLETED = "completed",
|
|
93
|
+
ARCHIVED = "archived"
|
|
94
|
+
}
|
|
95
|
+
interface TreatmentMetadata {
|
|
96
|
+
strategy?: string;
|
|
97
|
+
tone?: string;
|
|
98
|
+
ctaStyle?: string;
|
|
99
|
+
emotionalHook?: string;
|
|
100
|
+
reasoning?: string;
|
|
101
|
+
tags?: string[];
|
|
102
|
+
generated?: boolean;
|
|
103
|
+
generatedAt?: string;
|
|
104
|
+
}
|
|
105
|
+
interface VariantConfig {
|
|
106
|
+
content: string;
|
|
107
|
+
metadata?: TreatmentMetadata & Record<string, any>;
|
|
108
|
+
}
|
|
109
|
+
declare enum BanditAlgorithm {
|
|
110
|
+
EPSILON_GREEDY = "epsilon_greedy",
|
|
111
|
+
UCB1 = "ucb1",
|
|
112
|
+
THOMPSON_SAMPLING = "thompson_sampling",
|
|
113
|
+
CONTEXTUAL_LINEAR = "contextual_linear",
|
|
114
|
+
CONTEXTUAL_NEURAL = "contextual_neural"
|
|
115
|
+
}
|
|
116
|
+
declare enum ContentType {
|
|
117
|
+
PLAIN_TEXT = "plain_text",
|
|
118
|
+
MARKDOWN = "markdown",
|
|
119
|
+
HTML = "html",
|
|
120
|
+
CSS = "css",
|
|
121
|
+
JAVASCRIPT = "javascript",
|
|
122
|
+
JSON = "json",
|
|
123
|
+
IMAGE = "image",
|
|
124
|
+
VIDEO = "video",
|
|
125
|
+
AUDIO = "audio",
|
|
126
|
+
DOCUMENT = "document"
|
|
127
|
+
}
|
|
128
|
+
declare function getContentTypeCategory(contentType: ContentType): 'text' | 'code' | 'file';
|
|
129
|
+
interface BanditConfig {
|
|
130
|
+
algorithm: BanditAlgorithm;
|
|
131
|
+
parameters: Record<string, any>;
|
|
132
|
+
}
|
|
133
|
+
interface ExperimentStats {
|
|
134
|
+
totalAssignments: number;
|
|
135
|
+
totalConversions: number;
|
|
136
|
+
conversionRate: number;
|
|
137
|
+
treatmentStats: Record<string, TreatmentStats>;
|
|
138
|
+
}
|
|
139
|
+
interface TreatmentStats {
|
|
140
|
+
assignments: number;
|
|
141
|
+
conversions: number;
|
|
142
|
+
conversionRate: number;
|
|
143
|
+
confidence: number;
|
|
144
|
+
lift?: number;
|
|
145
|
+
}
|
|
146
|
+
interface APIResponse<T = any> {
|
|
147
|
+
success: boolean;
|
|
148
|
+
data?: T;
|
|
149
|
+
error?: string;
|
|
150
|
+
timestamp: Date;
|
|
151
|
+
}
|
|
152
|
+
interface CreateExperimentRequest {
|
|
153
|
+
name: string;
|
|
154
|
+
description?: string;
|
|
155
|
+
algorithm: BanditAlgorithm;
|
|
156
|
+
contentType: ContentType;
|
|
157
|
+
algorithmConfig?: Record<string, any>;
|
|
158
|
+
treatments?: Omit<Treatment, 'id' | 'experimentId' | 'createdAt'>[];
|
|
159
|
+
}
|
|
160
|
+
interface AssignmentRequest {
|
|
161
|
+
experimentId: string;
|
|
162
|
+
userId: string;
|
|
163
|
+
context?: ContextualFeatures;
|
|
164
|
+
sessionId?: string;
|
|
165
|
+
}
|
|
166
|
+
interface TrackNamedEventRequest {
|
|
167
|
+
name: string;
|
|
168
|
+
category?: string;
|
|
169
|
+
value?: number;
|
|
170
|
+
metadata?: Record<string, any>;
|
|
171
|
+
sessionId: string;
|
|
172
|
+
}
|
|
173
|
+
declare enum FunnelStatus {
|
|
174
|
+
DRAFT = "draft",
|
|
175
|
+
ACTIVE = "active",
|
|
176
|
+
PAUSED = "paused",
|
|
177
|
+
COMPLETED = "completed"
|
|
178
|
+
}
|
|
179
|
+
interface Funnel {
|
|
180
|
+
id: string;
|
|
181
|
+
name: string;
|
|
182
|
+
description?: string;
|
|
183
|
+
status: FunnelStatus;
|
|
184
|
+
conversionWindowMs: number;
|
|
185
|
+
companyId: string;
|
|
186
|
+
createdAt: Date;
|
|
187
|
+
updatedAt: Date;
|
|
188
|
+
}
|
|
189
|
+
interface FunnelRewardRequest {
|
|
190
|
+
funnelId: string;
|
|
191
|
+
userId: string;
|
|
192
|
+
value: number;
|
|
193
|
+
}
|
|
194
|
+
interface ApiKey {
|
|
195
|
+
id: string;
|
|
196
|
+
name: string;
|
|
197
|
+
keyPrefix: string;
|
|
198
|
+
status: 'active' | 'revoked';
|
|
199
|
+
createdBy: string;
|
|
200
|
+
createdAt: Date;
|
|
201
|
+
lastUsedAt?: Date;
|
|
202
|
+
revokedAt?: Date;
|
|
203
|
+
revokedBy?: string;
|
|
204
|
+
}
|
|
205
|
+
interface CreateApiKeyRequest {
|
|
206
|
+
name: string;
|
|
207
|
+
}
|
|
208
|
+
interface CreateApiKeyResponse {
|
|
209
|
+
id: string;
|
|
210
|
+
key: string;
|
|
211
|
+
name: string;
|
|
212
|
+
}
|
|
213
|
+
type IdentifyMergeReason = 'user_signup' | 'email_verification' | 'social_login' | 'account_recovery' | 'manual';
|
|
214
|
+
interface IdentifyUserRequest {
|
|
215
|
+
anonymousId: string;
|
|
216
|
+
identifiedUserId: string;
|
|
217
|
+
mergeReason?: IdentifyMergeReason;
|
|
218
|
+
}
|
|
219
|
+
interface IdentifyUserResponse {
|
|
220
|
+
anonymousId: string;
|
|
221
|
+
identifiedUserId: string;
|
|
222
|
+
alreadyMapped: boolean;
|
|
223
|
+
}
|
|
224
|
+
interface ResolvedIdentity {
|
|
225
|
+
canonicalId: string;
|
|
226
|
+
anonymousIds: string[];
|
|
227
|
+
isIdentified: boolean;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
declare function generateId(): string;
|
|
231
|
+
declare function validateTreatmentWeights(treatments: Treatment[]): boolean;
|
|
232
|
+
declare function normalizeTreatmentWeights(treatments: Treatment[]): Treatment[];
|
|
233
|
+
declare function calculateConfidenceInterval(successes: number, trials: number, confidence?: number): {
|
|
234
|
+
lower: number;
|
|
235
|
+
upper: number;
|
|
236
|
+
};
|
|
237
|
+
declare function isValidEmail(email: string): boolean;
|
|
238
|
+
declare function sanitizeMetadata(metadata: Record<string, any>): Record<string, any>;
|
|
239
|
+
/**
|
|
240
|
+
* Generates a URL-safe subdomain from a company name
|
|
241
|
+
* @param name Company name
|
|
242
|
+
* @returns URL-safe subdomain
|
|
243
|
+
*/
|
|
244
|
+
declare function generateSubdomain(name: string): string;
|
|
245
|
+
/**
|
|
246
|
+
* Validates if a subdomain is properly formatted
|
|
247
|
+
* @param subdomain Subdomain to validate
|
|
248
|
+
* @returns true if valid, false otherwise
|
|
249
|
+
*/
|
|
250
|
+
declare function isValidSubdomain(subdomain: string): boolean;
|
|
251
|
+
/**
|
|
252
|
+
* Extracts subdomain from hostname
|
|
253
|
+
* @param hostname Full hostname (e.g., 'acme.example.com')
|
|
254
|
+
* @param baseDomain Base domain (e.g., 'example.com')
|
|
255
|
+
* @returns Subdomain or null if not found
|
|
256
|
+
*/
|
|
257
|
+
declare function extractSubdomain(hostname: string, baseDomain: string): string | null;
|
|
258
|
+
/**
|
|
259
|
+
* Builds full URL with subdomain
|
|
260
|
+
* @param subdomain Subdomain
|
|
261
|
+
* @param baseDomain Base domain
|
|
262
|
+
* @param path Optional path
|
|
263
|
+
* @param protocol Protocol (default: https)
|
|
264
|
+
* @returns Full URL
|
|
265
|
+
*/
|
|
266
|
+
declare function buildSubdomainUrl(subdomain: string, baseDomain: string, path?: string, protocol?: string): string;
|
|
267
|
+
|
|
268
|
+
interface BanditsClientConfig {
|
|
269
|
+
apiUrl: string;
|
|
270
|
+
apiKey: string;
|
|
271
|
+
timeout?: number;
|
|
272
|
+
batchSize?: number;
|
|
273
|
+
flushInterval?: number;
|
|
274
|
+
sessionTtl?: number;
|
|
275
|
+
}
|
|
276
|
+
interface Assignment {
|
|
277
|
+
assignmentId: string;
|
|
278
|
+
treatmentId: string;
|
|
279
|
+
experimentId: string;
|
|
280
|
+
userId: string;
|
|
281
|
+
config: any;
|
|
282
|
+
}
|
|
283
|
+
declare class BanditsClient {
|
|
284
|
+
private config;
|
|
285
|
+
private namedEventQueue;
|
|
286
|
+
private sessionId;
|
|
287
|
+
private flushTimer?;
|
|
288
|
+
constructor(config: BanditsClientConfig);
|
|
289
|
+
getAssignment(experimentId: string, userId: string, context?: ContextualFeatures): Promise<Assignment>;
|
|
290
|
+
/**
|
|
291
|
+
* Track a named event for session-based attribution (recommended)
|
|
292
|
+
* Events are matched to bandits by sessionId — no assignmentId needed
|
|
293
|
+
* Set immediate: true to fire synchronously (survives navigation)
|
|
294
|
+
*/
|
|
295
|
+
trackNamedEvent(event: {
|
|
296
|
+
name: string;
|
|
297
|
+
category?: string;
|
|
298
|
+
value?: number;
|
|
299
|
+
metadata?: Record<string, any>;
|
|
300
|
+
}, opts?: {
|
|
301
|
+
immediate?: boolean;
|
|
302
|
+
}): void;
|
|
303
|
+
flush(): Promise<void>;
|
|
304
|
+
/**
|
|
305
|
+
* Link an anonymous user ID to a durable identity (e.g. email, account ID).
|
|
306
|
+
* All historical assignments and events for the anonymous ID are re-attributed
|
|
307
|
+
* to the identified user. Future calls to getAssignment() with either ID will
|
|
308
|
+
* resolve to the same assignment.
|
|
309
|
+
*
|
|
310
|
+
* Requires admin authentication — the client must be configured with a
|
|
311
|
+
* personal access token (PAT) instead of an API key, and companyId must
|
|
312
|
+
* be provided. This is a management operation, not an SDK client operation.
|
|
313
|
+
*
|
|
314
|
+
* @param anonymousId - The temporary/anonymous user ID (e.g. crypto.randomUUID())
|
|
315
|
+
* @param identifiedUserId - The permanent user ID (e.g. email, database ID)
|
|
316
|
+
* @param companyId - The company ID (required for PAT auth)
|
|
317
|
+
* @param mergeReason - Why the identification is happening
|
|
318
|
+
*/
|
|
319
|
+
identify(anonymousId: string, identifiedUserId: string, companyId: string, mergeReason?: 'user_signup' | 'email_verification' | 'social_login' | 'account_recovery' | 'manual'): Promise<void>;
|
|
320
|
+
/**
|
|
321
|
+
* Record a funnel payout event.
|
|
322
|
+
* Call this when the terminal conversion happens (e.g., purchase).
|
|
323
|
+
*
|
|
324
|
+
* @param funnelId - The funnel ID
|
|
325
|
+
* @param userId - The user who converted
|
|
326
|
+
* @param value - The payout value (e.g., purchase amount in dollars)
|
|
327
|
+
*/
|
|
328
|
+
trackFunnelReward(funnelId: string, userId: string, value: number): Promise<void>;
|
|
329
|
+
private generateId;
|
|
330
|
+
private getOrCreateSessionId;
|
|
331
|
+
private makeRequest;
|
|
332
|
+
private startBatchFlush;
|
|
333
|
+
destroy(): void;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export { type APIResponse, type Account, type AlgorithmResult, type ApiKey, type Assignment, type AssignmentRequest, BanditAlgorithm, BanditsClient as BanditClient, type BanditConfig, type BanditResult, BanditsClient, type BanditsClientConfig, type Company, ContentType, type ContextualFeatures, type CreateApiKeyRequest, type CreateApiKeyResponse, type CreateExperimentRequest, type Experiment, type ExperimentStats, ExperimentStatus, type Funnel, type FunnelRewardRequest, FunnelStatus, type IdentifyMergeReason, type IdentifyUserRequest, type IdentifyUserResponse, type ResolvedIdentity, type Session, type TrackNamedEventRequest, type Treatment, type TreatmentMetadata, type TreatmentStats, type User, type VariantConfig, buildSubdomainUrl, calculateConfidenceInterval, extractSubdomain, generateId, generateSubdomain, getContentTypeCategory, isValidEmail, isValidSubdomain, normalizeTreatmentWeights, sanitizeMetadata, validateTreatmentWeights };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var BanditsClient = class {
|
|
3
|
+
config;
|
|
4
|
+
namedEventQueue = [];
|
|
5
|
+
sessionId;
|
|
6
|
+
flushTimer;
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.config = {
|
|
9
|
+
timeout: 5e3,
|
|
10
|
+
batchSize: 100,
|
|
11
|
+
flushInterval: 1e4,
|
|
12
|
+
sessionTtl: 864e5,
|
|
13
|
+
// 24 hours in ms
|
|
14
|
+
...config
|
|
15
|
+
};
|
|
16
|
+
this.sessionId = this.getOrCreateSessionId();
|
|
17
|
+
this.startBatchFlush();
|
|
18
|
+
}
|
|
19
|
+
async getAssignment(experimentId, userId, context) {
|
|
20
|
+
const request = {
|
|
21
|
+
experimentId,
|
|
22
|
+
userId,
|
|
23
|
+
context,
|
|
24
|
+
sessionId: this.sessionId
|
|
25
|
+
};
|
|
26
|
+
const response = await this.makeRequest(
|
|
27
|
+
"/assignments",
|
|
28
|
+
"POST",
|
|
29
|
+
request
|
|
30
|
+
);
|
|
31
|
+
if (!response.success || !response.data) {
|
|
32
|
+
throw new Error(response.error || "Failed to get assignment");
|
|
33
|
+
}
|
|
34
|
+
const assignment = response.data?.data ?? response.data;
|
|
35
|
+
if (!assignment?.assignmentId) {
|
|
36
|
+
throw new Error("Invalid assignment response");
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
assignmentId: assignment.assignmentId,
|
|
40
|
+
treatmentId: assignment.treatmentId,
|
|
41
|
+
experimentId,
|
|
42
|
+
userId,
|
|
43
|
+
config: assignment.treatment?.config || {}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Track a named event for session-based attribution (recommended)
|
|
48
|
+
* Events are matched to bandits by sessionId — no assignmentId needed
|
|
49
|
+
* Set immediate: true to fire synchronously (survives navigation)
|
|
50
|
+
*/
|
|
51
|
+
trackNamedEvent(event, opts) {
|
|
52
|
+
const request = {
|
|
53
|
+
name: event.name,
|
|
54
|
+
category: event.category || "custom",
|
|
55
|
+
value: event.value,
|
|
56
|
+
metadata: event.metadata,
|
|
57
|
+
sessionId: this.sessionId
|
|
58
|
+
};
|
|
59
|
+
if (opts?.immediate) {
|
|
60
|
+
const url = `${this.config.apiUrl}/events`;
|
|
61
|
+
try {
|
|
62
|
+
void fetch(url, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
66
|
+
"Content-Type": "application/json"
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify(request),
|
|
69
|
+
keepalive: true
|
|
70
|
+
});
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
this.namedEventQueue.push(request);
|
|
76
|
+
if (this.namedEventQueue.length >= this.config.batchSize) {
|
|
77
|
+
this.flush();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async flush() {
|
|
81
|
+
const namedEvents = [...this.namedEventQueue];
|
|
82
|
+
this.namedEventQueue = [];
|
|
83
|
+
if (namedEvents.length === 0) return;
|
|
84
|
+
try {
|
|
85
|
+
await fetch(`${this.config.apiUrl}/events/batch`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
89
|
+
"Content-Type": "application/json"
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({ events: namedEvents }),
|
|
92
|
+
keepalive: true
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
this.namedEventQueue.unshift(...namedEvents);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Link an anonymous user ID to a durable identity (e.g. email, account ID).
|
|
101
|
+
* All historical assignments and events for the anonymous ID are re-attributed
|
|
102
|
+
* to the identified user. Future calls to getAssignment() with either ID will
|
|
103
|
+
* resolve to the same assignment.
|
|
104
|
+
*
|
|
105
|
+
* Requires admin authentication — the client must be configured with a
|
|
106
|
+
* personal access token (PAT) instead of an API key, and companyId must
|
|
107
|
+
* be provided. This is a management operation, not an SDK client operation.
|
|
108
|
+
*
|
|
109
|
+
* @param anonymousId - The temporary/anonymous user ID (e.g. crypto.randomUUID())
|
|
110
|
+
* @param identifiedUserId - The permanent user ID (e.g. email, database ID)
|
|
111
|
+
* @param companyId - The company ID (required for PAT auth)
|
|
112
|
+
* @param mergeReason - Why the identification is happening
|
|
113
|
+
*/
|
|
114
|
+
async identify(anonymousId, identifiedUserId, companyId, mergeReason) {
|
|
115
|
+
const response = await this.makeRequest(
|
|
116
|
+
"/identify",
|
|
117
|
+
"POST",
|
|
118
|
+
{
|
|
119
|
+
anonymousId,
|
|
120
|
+
identifiedUserId,
|
|
121
|
+
mergeReason: mergeReason || "manual"
|
|
122
|
+
},
|
|
123
|
+
{ "x-company-id": companyId }
|
|
124
|
+
);
|
|
125
|
+
if (!response.success) {
|
|
126
|
+
throw new Error(response.error || "Failed to identify user");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Record a funnel payout event.
|
|
131
|
+
* Call this when the terminal conversion happens (e.g., purchase).
|
|
132
|
+
*
|
|
133
|
+
* @param funnelId - The funnel ID
|
|
134
|
+
* @param userId - The user who converted
|
|
135
|
+
* @param value - The payout value (e.g., purchase amount in dollars)
|
|
136
|
+
*/
|
|
137
|
+
async trackFunnelReward(funnelId, userId, value) {
|
|
138
|
+
const response = await this.makeRequest(
|
|
139
|
+
`/funnels/${funnelId}/reward`,
|
|
140
|
+
"POST",
|
|
141
|
+
{ userId, value }
|
|
142
|
+
);
|
|
143
|
+
if (!response.success) {
|
|
144
|
+
throw new Error(response.error || "Failed to record funnel reward");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
generateId() {
|
|
148
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
149
|
+
return crypto.randomUUID();
|
|
150
|
+
}
|
|
151
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
152
|
+
const r = Math.random() * 16 | 0;
|
|
153
|
+
return (c === "x" ? r : r & 3 | 8).toString(16);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
getOrCreateSessionId() {
|
|
157
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
158
|
+
const stored = localStorage.getItem("bandits_session_id");
|
|
159
|
+
const storedAt = localStorage.getItem("bandits_session_created_at");
|
|
160
|
+
if (stored && storedAt) {
|
|
161
|
+
const age = Date.now() - parseInt(storedAt, 10);
|
|
162
|
+
if (age < this.config.sessionTtl) {
|
|
163
|
+
return stored;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const id = this.generateId();
|
|
167
|
+
localStorage.setItem("bandits_session_id", id);
|
|
168
|
+
localStorage.setItem("bandits_session_created_at", String(Date.now()));
|
|
169
|
+
return id;
|
|
170
|
+
}
|
|
171
|
+
return this.generateId();
|
|
172
|
+
}
|
|
173
|
+
async makeRequest(endpoint, method, body, extraHeaders) {
|
|
174
|
+
const url = `${this.config.apiUrl}${endpoint}`;
|
|
175
|
+
const headers = {
|
|
176
|
+
"Authorization": `Bearer ${this.config.apiKey}`,
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
...extraHeaders
|
|
179
|
+
};
|
|
180
|
+
const requestConfig = {
|
|
181
|
+
method,
|
|
182
|
+
headers,
|
|
183
|
+
signal: AbortSignal.timeout(this.config.timeout)
|
|
184
|
+
};
|
|
185
|
+
if (body && method !== "GET") {
|
|
186
|
+
requestConfig.body = JSON.stringify(body);
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const response = await fetch(url, requestConfig);
|
|
190
|
+
const data = await response.json();
|
|
191
|
+
return {
|
|
192
|
+
success: response.ok,
|
|
193
|
+
data: response.ok ? data : void 0,
|
|
194
|
+
error: response.ok ? void 0 : data?.message || "Request failed",
|
|
195
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return {
|
|
199
|
+
success: false,
|
|
200
|
+
error: error instanceof Error ? error.message : "Network error",
|
|
201
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
startBatchFlush() {
|
|
206
|
+
this.flushTimer = setInterval(() => {
|
|
207
|
+
this.flush().catch(console.error);
|
|
208
|
+
}, this.config.flushInterval);
|
|
209
|
+
}
|
|
210
|
+
destroy() {
|
|
211
|
+
if (this.flushTimer) {
|
|
212
|
+
clearInterval(this.flushTimer);
|
|
213
|
+
}
|
|
214
|
+
this.flush().catch(console.error);
|
|
215
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
216
|
+
localStorage.removeItem("bandits_session_id");
|
|
217
|
+
localStorage.removeItem("bandits_session_created_at");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// ../shared/dist/types.js
|
|
223
|
+
var ExperimentStatus;
|
|
224
|
+
(function(ExperimentStatus2) {
|
|
225
|
+
ExperimentStatus2["DRAFT"] = "draft";
|
|
226
|
+
ExperimentStatus2["ACTIVE"] = "active";
|
|
227
|
+
ExperimentStatus2["PAUSED"] = "paused";
|
|
228
|
+
ExperimentStatus2["COMPLETED"] = "completed";
|
|
229
|
+
ExperimentStatus2["ARCHIVED"] = "archived";
|
|
230
|
+
})(ExperimentStatus || (ExperimentStatus = {}));
|
|
231
|
+
var BanditAlgorithm;
|
|
232
|
+
(function(BanditAlgorithm2) {
|
|
233
|
+
BanditAlgorithm2["EPSILON_GREEDY"] = "epsilon_greedy";
|
|
234
|
+
BanditAlgorithm2["UCB1"] = "ucb1";
|
|
235
|
+
BanditAlgorithm2["THOMPSON_SAMPLING"] = "thompson_sampling";
|
|
236
|
+
BanditAlgorithm2["CONTEXTUAL_LINEAR"] = "contextual_linear";
|
|
237
|
+
BanditAlgorithm2["CONTEXTUAL_NEURAL"] = "contextual_neural";
|
|
238
|
+
})(BanditAlgorithm || (BanditAlgorithm = {}));
|
|
239
|
+
var ContentType;
|
|
240
|
+
(function(ContentType2) {
|
|
241
|
+
ContentType2["PLAIN_TEXT"] = "plain_text";
|
|
242
|
+
ContentType2["MARKDOWN"] = "markdown";
|
|
243
|
+
ContentType2["HTML"] = "html";
|
|
244
|
+
ContentType2["CSS"] = "css";
|
|
245
|
+
ContentType2["JAVASCRIPT"] = "javascript";
|
|
246
|
+
ContentType2["JSON"] = "json";
|
|
247
|
+
ContentType2["IMAGE"] = "image";
|
|
248
|
+
ContentType2["VIDEO"] = "video";
|
|
249
|
+
ContentType2["AUDIO"] = "audio";
|
|
250
|
+
ContentType2["DOCUMENT"] = "document";
|
|
251
|
+
})(ContentType || (ContentType = {}));
|
|
252
|
+
function getContentTypeCategory(contentType) {
|
|
253
|
+
switch (contentType) {
|
|
254
|
+
case ContentType.PLAIN_TEXT:
|
|
255
|
+
case ContentType.MARKDOWN:
|
|
256
|
+
return "text";
|
|
257
|
+
case ContentType.HTML:
|
|
258
|
+
case ContentType.CSS:
|
|
259
|
+
case ContentType.JAVASCRIPT:
|
|
260
|
+
case ContentType.JSON:
|
|
261
|
+
return "code";
|
|
262
|
+
case ContentType.IMAGE:
|
|
263
|
+
case ContentType.VIDEO:
|
|
264
|
+
case ContentType.AUDIO:
|
|
265
|
+
case ContentType.DOCUMENT:
|
|
266
|
+
return "file";
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
var FunnelStatus;
|
|
270
|
+
(function(FunnelStatus2) {
|
|
271
|
+
FunnelStatus2["DRAFT"] = "draft";
|
|
272
|
+
FunnelStatus2["ACTIVE"] = "active";
|
|
273
|
+
FunnelStatus2["PAUSED"] = "paused";
|
|
274
|
+
FunnelStatus2["COMPLETED"] = "completed";
|
|
275
|
+
})(FunnelStatus || (FunnelStatus = {}));
|
|
276
|
+
|
|
277
|
+
// ../shared/dist/utils.js
|
|
278
|
+
function generateId() {
|
|
279
|
+
return Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
280
|
+
}
|
|
281
|
+
function validateTreatmentWeights(treatments) {
|
|
282
|
+
const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);
|
|
283
|
+
return totalWeight > 0;
|
|
284
|
+
}
|
|
285
|
+
function normalizeTreatmentWeights(treatments) {
|
|
286
|
+
const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);
|
|
287
|
+
return treatments.map((treatment) => ({
|
|
288
|
+
...treatment,
|
|
289
|
+
weight: (treatment.weight || 1) / totalWeight
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
function calculateConfidenceInterval(successes, trials, confidence = 0.95) {
|
|
293
|
+
if (trials === 0)
|
|
294
|
+
return { lower: 0, upper: 0 };
|
|
295
|
+
const z = confidence === 0.95 ? 1.96 : confidence === 0.99 ? 2.576 : 1.645;
|
|
296
|
+
const p = successes / trials;
|
|
297
|
+
const margin = z * Math.sqrt(p * (1 - p) / trials);
|
|
298
|
+
return {
|
|
299
|
+
lower: Math.max(0, p - margin),
|
|
300
|
+
upper: Math.min(1, p + margin)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function isValidEmail(email) {
|
|
304
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
305
|
+
return emailRegex.test(email);
|
|
306
|
+
}
|
|
307
|
+
function sanitizeMetadata(metadata) {
|
|
308
|
+
const sanitized = {};
|
|
309
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
310
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
311
|
+
sanitized[key] = value;
|
|
312
|
+
} else if (value !== null && value !== void 0) {
|
|
313
|
+
sanitized[key] = String(value);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return sanitized;
|
|
317
|
+
}
|
|
318
|
+
function generateSubdomain(name) {
|
|
319
|
+
return name.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 63);
|
|
320
|
+
}
|
|
321
|
+
function isValidSubdomain(subdomain) {
|
|
322
|
+
if (!subdomain || subdomain.length === 0 || subdomain.length > 63) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
const subdomainRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
326
|
+
const reserved = [
|
|
327
|
+
"www",
|
|
328
|
+
"api",
|
|
329
|
+
"app",
|
|
330
|
+
"admin",
|
|
331
|
+
"dashboard",
|
|
332
|
+
"mail",
|
|
333
|
+
"ftp",
|
|
334
|
+
"localhost",
|
|
335
|
+
"staging",
|
|
336
|
+
"dev",
|
|
337
|
+
"test",
|
|
338
|
+
"demo",
|
|
339
|
+
"blog",
|
|
340
|
+
"docs",
|
|
341
|
+
"status",
|
|
342
|
+
"help",
|
|
343
|
+
"support",
|
|
344
|
+
"cdn",
|
|
345
|
+
"static",
|
|
346
|
+
"assets",
|
|
347
|
+
"secure",
|
|
348
|
+
"payment",
|
|
349
|
+
"pay"
|
|
350
|
+
];
|
|
351
|
+
return subdomainRegex.test(subdomain) && !reserved.includes(subdomain);
|
|
352
|
+
}
|
|
353
|
+
function extractSubdomain(hostname, baseDomain) {
|
|
354
|
+
if (!hostname || !baseDomain) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const cleanHostname = hostname.split(":")[0];
|
|
358
|
+
if (!cleanHostname.endsWith(baseDomain)) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const subdomain = cleanHostname.slice(0, -(baseDomain.length + 1));
|
|
362
|
+
if (!subdomain || subdomain === "www") {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
return subdomain;
|
|
366
|
+
}
|
|
367
|
+
function buildSubdomainUrl(subdomain, baseDomain, path = "", protocol = "https") {
|
|
368
|
+
const url = `${protocol}://${subdomain}.${baseDomain}`;
|
|
369
|
+
return path ? `${url}${path.startsWith("/") ? path : "/" + path}` : url;
|
|
370
|
+
}
|
|
371
|
+
export {
|
|
372
|
+
BanditAlgorithm,
|
|
373
|
+
BanditsClient as BanditClient,
|
|
374
|
+
BanditsClient,
|
|
375
|
+
ContentType,
|
|
376
|
+
ExperimentStatus,
|
|
377
|
+
FunnelStatus,
|
|
378
|
+
buildSubdomainUrl,
|
|
379
|
+
calculateConfidenceInterval,
|
|
380
|
+
extractSubdomain,
|
|
381
|
+
generateId,
|
|
382
|
+
generateSubdomain,
|
|
383
|
+
getContentTypeCategory,
|
|
384
|
+
isValidEmail,
|
|
385
|
+
isValidSubdomain,
|
|
386
|
+
normalizeTreatmentWeights,
|
|
387
|
+
sanitizeMetadata,
|
|
388
|
+
validateTreatmentWeights
|
|
389
|
+
};
|
|
390
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../../shared/src/types.ts","../../shared/src/utils.ts"],"sourcesContent":["import {\n AssignmentRequest,\n TrackNamedEventRequest,\n BanditResult,\n APIResponse,\n ContextualFeatures\n} from '@runbandit/shared';\n\nexport interface BanditsClientConfig {\n apiUrl: string;\n apiKey: string;\n timeout?: number;\n batchSize?: number;\n flushInterval?: number;\n sessionTtl?: number; // default 24 hours in ms\n}\n\nexport interface Assignment {\n assignmentId: string;\n treatmentId: string;\n experimentId: string;\n userId: string;\n config: any;\n}\n\nexport class BanditsClient {\n private config: BanditsClientConfig;\n private namedEventQueue: TrackNamedEventRequest[] = [];\n private sessionId: string;\n private flushTimer?: NodeJS.Timeout;\n\n constructor(config: BanditsClientConfig) {\n this.config = {\n timeout: 5000,\n batchSize: 100,\n flushInterval: 10000,\n sessionTtl: 86400000, // 24 hours in ms\n ...config\n };\n this.sessionId = this.getOrCreateSessionId();\n this.startBatchFlush();\n }\n\n async getAssignment(\n experimentId: string,\n userId: string,\n context?: ContextualFeatures\n ): Promise<Assignment> {\n const request: AssignmentRequest = {\n experimentId,\n userId,\n context,\n sessionId: this.sessionId\n };\n\n const response = await this.makeRequest<any>(\n '/assignments',\n 'POST',\n request\n );\n\n if (!response.success || !response.data) {\n throw new Error(response.error || 'Failed to get assignment');\n }\n\n // API returns { success, data: { assignmentId, treatmentId, treatment, confidence } }\n // makeRequest puts the raw JSON body in response.data, so unwrap one level.\n const assignment = response.data?.data ?? response.data;\n\n if (!assignment?.assignmentId) {\n throw new Error('Invalid assignment response');\n }\n\n return {\n assignmentId: assignment.assignmentId,\n treatmentId: assignment.treatmentId,\n experimentId,\n userId,\n config: assignment.treatment?.config || {}\n };\n }\n\n /**\n * Track a named event for session-based attribution (recommended)\n * Events are matched to bandits by sessionId — no assignmentId needed\n * Set immediate: true to fire synchronously (survives navigation)\n */\n trackNamedEvent(\n event: { name: string; category?: string; value?: number; metadata?: Record<string, any> },\n opts?: { immediate?: boolean }\n ): void {\n const request: TrackNamedEventRequest = {\n name: event.name,\n category: event.category || 'custom',\n value: event.value,\n metadata: event.metadata,\n sessionId: this.sessionId,\n };\n\n if (opts?.immediate) {\n // Fire now and survive navigation (e.g. CTA click -> route change)\n const url = `${this.config.apiUrl}/events`;\n try {\n void fetch(url, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.config.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n keepalive: true,\n });\n } catch {\n // best-effort\n }\n return;\n }\n\n this.namedEventQueue.push(request);\n if (this.namedEventQueue.length >= this.config.batchSize!) {\n this.flush();\n }\n }\n\n async flush(): Promise<void> {\n const namedEvents = [...this.namedEventQueue];\n this.namedEventQueue = [];\n if (namedEvents.length === 0) return;\n\n try {\n await fetch(`${this.config.apiUrl}/events/batch`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.config.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events: namedEvents }),\n keepalive: true,\n });\n } catch (error) {\n this.namedEventQueue.unshift(...namedEvents);\n throw error;\n }\n }\n\n /**\n * Link an anonymous user ID to a durable identity (e.g. email, account ID).\n * All historical assignments and events for the anonymous ID are re-attributed\n * to the identified user. Future calls to getAssignment() with either ID will\n * resolve to the same assignment.\n *\n * Requires admin authentication — the client must be configured with a\n * personal access token (PAT) instead of an API key, and companyId must\n * be provided. This is a management operation, not an SDK client operation.\n *\n * @param anonymousId - The temporary/anonymous user ID (e.g. crypto.randomUUID())\n * @param identifiedUserId - The permanent user ID (e.g. email, database ID)\n * @param companyId - The company ID (required for PAT auth)\n * @param mergeReason - Why the identification is happening\n */\n async identify(\n anonymousId: string,\n identifiedUserId: string,\n companyId: string,\n mergeReason?: 'user_signup' | 'email_verification' | 'social_login' | 'account_recovery' | 'manual'\n ): Promise<void> {\n const response = await this.makeRequest(\n '/identify',\n 'POST',\n {\n anonymousId,\n identifiedUserId,\n mergeReason: mergeReason || 'manual',\n },\n { 'x-company-id': companyId }\n );\n\n if (!response.success) {\n throw new Error(response.error || 'Failed to identify user');\n }\n }\n\n /**\n * Record a funnel payout event.\n * Call this when the terminal conversion happens (e.g., purchase).\n *\n * @param funnelId - The funnel ID\n * @param userId - The user who converted\n * @param value - The payout value (e.g., purchase amount in dollars)\n */\n async trackFunnelReward(\n funnelId: string,\n userId: string,\n value: number\n ): Promise<void> {\n const response = await this.makeRequest(\n `/funnels/${funnelId}/reward`,\n 'POST',\n { userId, value }\n );\n\n if (!response.success) {\n throw new Error(response.error || 'Failed to record funnel reward');\n }\n }\n\n private generateId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n }\n\n private getOrCreateSessionId(): string {\n if (typeof window !== 'undefined' && window.localStorage) {\n const stored = localStorage.getItem('bandits_session_id');\n const storedAt = localStorage.getItem('bandits_session_created_at');\n if (stored && storedAt) {\n const age = Date.now() - parseInt(storedAt, 10);\n if (age < this.config.sessionTtl!) {\n return stored;\n }\n }\n const id = this.generateId();\n localStorage.setItem('bandits_session_id', id);\n localStorage.setItem('bandits_session_created_at', String(Date.now()));\n return id;\n }\n return this.generateId();\n }\n\n private async makeRequest<T = any>(\n endpoint: string,\n method: 'GET' | 'POST' | 'PUT' | 'DELETE',\n body?: any,\n extraHeaders?: Record<string, string>\n ): Promise<APIResponse<T>> {\n const url = `${this.config.apiUrl}${endpoint}`;\n\n const headers: Record<string, string> = {\n 'Authorization': `Bearer ${this.config.apiKey}`,\n 'Content-Type': 'application/json',\n ...extraHeaders,\n };\n\n const requestConfig: RequestInit = {\n method,\n headers,\n signal: AbortSignal.timeout(this.config.timeout!)\n };\n\n if (body && method !== 'GET') {\n requestConfig.body = JSON.stringify(body);\n }\n\n try {\n const response = await fetch(url, requestConfig);\n const data = await response.json();\n \n return {\n success: response.ok,\n data: response.ok ? data as T : undefined,\n error: response.ok ? undefined : (data as any)?.message || 'Request failed',\n timestamp: new Date()\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Network error',\n timestamp: new Date()\n };\n }\n }\n\n private startBatchFlush(): void {\n this.flushTimer = setInterval(() => {\n this.flush().catch(console.error);\n }, this.config.flushInterval);\n }\n\n destroy(): void {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n }\n this.flush().catch(console.error);\n if (typeof window !== 'undefined' && window.localStorage) {\n localStorage.removeItem('bandits_session_id');\n localStorage.removeItem('bandits_session_created_at');\n }\n }\n}","export interface Company {\n id: string;\n name: string;\n subdomain?: string; // nullable for backwards compatibility with existing data\n domain?: string;\n plan: string;\n apiKey: string;\n status: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface User {\n id: string;\n email: string;\n name: string;\n image?: string;\n role: string;\n companyId: string | null;\n emailVerified?: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Session {\n id: string;\n userId: string;\n token: string;\n expiresAt: Date;\n ipAddress?: string | null;\n userAgent?: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Account {\n id: string;\n userId: string;\n provider: string;\n providerAccountId: string;\n accessToken?: string;\n refreshToken?: string;\n accessTokenExpiresAt?: Date;\n refreshTokenExpiresAt?: Date;\n scope?: string;\n idToken?: string;\n sessionState?: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Experiment {\n id: string;\n name: string;\n description?: string;\n algorithm: BanditAlgorithm;\n status: ExperimentStatus;\n contentType: ContentType;\n treatments: Treatment[];\n context?: Record<string, any>;\n image?: string; // URL for uploaded image\n emoji?: string; // Emoji character as alternative to image\n assignmentTtl: number; // TTL in seconds for assignment expiration\n companyId: string;\n createdAt: Date;\n updatedAt: Date;\n endAt?: Date;\n}\n\nexport interface Treatment {\n id: string;\n experimentId: string;\n name: string;\n description?: string;\n config: VariantConfig;\n weight?: number;\n companyId: string;\n createdAt: Date;\n}\n\nexport interface AlgorithmResult {\n treatmentId: string;\n confidence: number;\n}\n\nexport interface BanditResult {\n assignmentId: string;\n treatmentId: string;\n confidence: number;\n metadata?: Record<string, any>;\n}\n\nexport interface ContextualFeatures {\n [key: string]: string | number | boolean;\n}\n\nexport enum ExperimentStatus {\n DRAFT = 'draft',\n ACTIVE = 'active',\n PAUSED = 'paused',\n COMPLETED = 'completed',\n ARCHIVED = 'archived'\n}\n\n\nexport interface TreatmentMetadata {\n strategy?: string; // e.g. \"social proof\", \"urgency\"\n tone?: string; // e.g. \"warm\", \"snarky\", \"deadpan corporate\"\n ctaStyle?: string; // e.g. \"direct\", \"soft question\", \"implied next step\"\n emotionalHook?: string; // e.g. \"fomo\", \"belonging\", \"righteous indignation\"\n reasoning?: string; // LLM's explanation of why it chose this approach\n tags?: string[];\n generated?: boolean;\n generatedAt?: string;\n}\n\nexport interface VariantConfig {\n content: string;\n metadata?: TreatmentMetadata & Record<string, any>;\n}\n\nexport enum BanditAlgorithm {\n EPSILON_GREEDY = 'epsilon_greedy',\n UCB1 = 'ucb1',\n THOMPSON_SAMPLING = 'thompson_sampling',\n CONTEXTUAL_LINEAR = 'contextual_linear',\n CONTEXTUAL_NEURAL = 'contextual_neural'\n}\n\nexport enum ContentType {\n PLAIN_TEXT = 'plain_text',\n MARKDOWN = 'markdown',\n HTML = 'html',\n CSS = 'css',\n JAVASCRIPT = 'javascript',\n JSON = 'json',\n IMAGE = 'image',\n VIDEO = 'video',\n AUDIO = 'audio',\n DOCUMENT = 'document',\n}\n\nexport function getContentTypeCategory(contentType: ContentType): 'text' | 'code' | 'file' {\n switch (contentType) {\n case ContentType.PLAIN_TEXT:\n case ContentType.MARKDOWN:\n return 'text';\n case ContentType.HTML:\n case ContentType.CSS:\n case ContentType.JAVASCRIPT:\n case ContentType.JSON:\n return 'code';\n case ContentType.IMAGE:\n case ContentType.VIDEO:\n case ContentType.AUDIO:\n case ContentType.DOCUMENT:\n return 'file';\n }\n}\n\nexport interface BanditConfig {\n algorithm: BanditAlgorithm;\n parameters: Record<string, any>;\n}\n\nexport interface ExperimentStats {\n totalAssignments: number;\n totalConversions: number;\n conversionRate: number;\n treatmentStats: Record<string, TreatmentStats>;\n}\n\nexport interface TreatmentStats {\n assignments: number;\n conversions: number;\n conversionRate: number;\n confidence: number;\n lift?: number;\n}\n\nexport interface APIResponse<T = any> {\n success: boolean;\n data?: T;\n error?: string;\n timestamp: Date;\n}\n\nexport interface CreateExperimentRequest {\n name: string;\n description?: string;\n algorithm: BanditAlgorithm;\n contentType: ContentType;\n algorithmConfig?: Record<string, any>;\n treatments?: Omit<Treatment, 'id' | 'experimentId' | 'createdAt'>[];\n}\n\nexport interface AssignmentRequest {\n experimentId: string;\n userId: string;\n context?: ContextualFeatures;\n sessionId?: string;\n}\n\n\nexport interface TrackNamedEventRequest {\n name: string;\n category?: string;\n value?: number;\n metadata?: Record<string, any>;\n sessionId: string;\n}\n\n// --- Funnel RL Types ---\n\nexport enum FunnelStatus {\n DRAFT = 'draft',\n ACTIVE = 'active',\n PAUSED = 'paused',\n COMPLETED = 'completed',\n}\n\nexport interface Funnel {\n id: string;\n name: string;\n description?: string;\n status: FunnelStatus;\n conversionWindowMs: number;\n companyId: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface FunnelRewardRequest {\n funnelId: string;\n userId: string;\n value: number;\n}\n\n// --- API Key Management Types ---\n\nexport interface ApiKey {\n id: string;\n name: string;\n keyPrefix: string;\n status: 'active' | 'revoked';\n createdBy: string;\n createdAt: Date;\n lastUsedAt?: Date;\n revokedAt?: Date;\n revokedBy?: string;\n}\n\nexport interface CreateApiKeyRequest {\n name: string;\n}\n\nexport interface CreateApiKeyResponse {\n id: string;\n key: string;\n name: string;\n}\n\n// --- Identity Resolution Types ---\n\nexport type IdentifyMergeReason =\n | 'user_signup'\n | 'email_verification'\n | 'social_login'\n | 'account_recovery'\n | 'manual';\n\nexport interface IdentifyUserRequest {\n anonymousId: string;\n identifiedUserId: string;\n mergeReason?: IdentifyMergeReason;\n}\n\nexport interface IdentifyUserResponse {\n anonymousId: string;\n identifiedUserId: string;\n alreadyMapped: boolean;\n}\n\nexport interface ResolvedIdentity {\n canonicalId: string;\n anonymousIds: string[];\n isIdentified: boolean;\n}\n","import { Treatment } from './types.js';\n\nexport function generateId(): string {\n return Math.random().toString(36).substring(2) + Date.now().toString(36);\n}\n\nexport function validateTreatmentWeights(treatments: Treatment[]): boolean {\n const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);\n return totalWeight > 0;\n}\n\nexport function normalizeTreatmentWeights(treatments: Treatment[]): Treatment[] {\n const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);\n \n return treatments.map(treatment => ({\n ...treatment,\n weight: (treatment.weight || 1) / totalWeight\n }));\n}\n\nexport function calculateConfidenceInterval(\n successes: number,\n trials: number,\n confidence: number = 0.95\n): { lower: number; upper: number } {\n if (trials === 0) return { lower: 0, upper: 0 };\n \n const z = confidence === 0.95 ? 1.96 : confidence === 0.99 ? 2.576 : 1.645;\n const p = successes / trials;\n const margin = z * Math.sqrt((p * (1 - p)) / trials);\n \n return {\n lower: Math.max(0, p - margin),\n upper: Math.min(1, p + margin)\n };\n}\n\nexport function isValidEmail(email: string): boolean {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return emailRegex.test(email);\n}\n\nexport function sanitizeMetadata(metadata: Record<string, any>): Record<string, any> {\n const sanitized: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n sanitized[key] = value;\n } else if (value !== null && value !== undefined) {\n sanitized[key] = String(value);\n }\n }\n\n return sanitized;\n}\n\n/**\n * Generates a URL-safe subdomain from a company name\n * @param name Company name\n * @returns URL-safe subdomain\n */\nexport function generateSubdomain(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '') // Remove special characters\n .replace(/\\s+/g, '-') // Replace spaces with hyphens\n .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen\n .replace(/^-|-$/g, '') // Remove leading/trailing hyphens\n .substring(0, 63); // DNS label max length\n}\n\n/**\n * Validates if a subdomain is properly formatted\n * @param subdomain Subdomain to validate\n * @returns true if valid, false otherwise\n */\nexport function isValidSubdomain(subdomain: string): boolean {\n if (!subdomain || subdomain.length === 0 || subdomain.length > 63) {\n return false;\n }\n\n // Subdomain rules: lowercase letters, numbers, hyphens\n // Cannot start or end with hyphen\n // Cannot contain consecutive hyphens\n const subdomainRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;\n\n // Reserved subdomains\n const reserved = [\n 'www', 'api', 'app', 'admin', 'dashboard', 'mail', 'ftp', 'localhost',\n 'staging', 'dev', 'test', 'demo', 'blog', 'docs', 'status', 'help',\n 'support', 'cdn', 'static', 'assets', 'secure', 'payment', 'pay'\n ];\n\n return subdomainRegex.test(subdomain) && !reserved.includes(subdomain);\n}\n\n/**\n * Extracts subdomain from hostname\n * @param hostname Full hostname (e.g., 'acme.example.com')\n * @param baseDomain Base domain (e.g., 'example.com')\n * @returns Subdomain or null if not found\n */\nexport function extractSubdomain(hostname: string, baseDomain: string): string | null {\n if (!hostname || !baseDomain) {\n return null;\n }\n\n // Remove port if present\n const cleanHostname = hostname.split(':')[0];\n\n // Check if hostname ends with base domain\n if (!cleanHostname.endsWith(baseDomain)) {\n return null;\n }\n\n // Extract subdomain part\n const subdomain = cleanHostname.slice(0, -(baseDomain.length + 1));\n\n // Return null if no subdomain or if it's www\n if (!subdomain || subdomain === 'www') {\n return null;\n }\n\n return subdomain;\n}\n\n/**\n * Builds full URL with subdomain\n * @param subdomain Subdomain\n * @param baseDomain Base domain\n * @param path Optional path\n * @param protocol Protocol (default: https)\n * @returns Full URL\n */\nexport function buildSubdomainUrl(\n subdomain: string,\n baseDomain: string,\n path: string = '',\n protocol: string = 'https'\n): string {\n const url = `${protocol}://${subdomain}.${baseDomain}`;\n return path ? `${url}${path.startsWith('/') ? path : '/' + path}` : url;\n}"],"mappings":"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","names":["ExperimentStatus","BanditAlgorithm","ContentType","FunnelStatus"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@runbandit/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"clean": "rm -rf dist"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@runbandit/shared": "workspace:*",
|
|
26
|
+
"tsup": "^8.0.0",
|
|
27
|
+
"typescript": "^5.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|