@prosdevlab/experience-sdk 0.1.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.
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
export { bannerPlugin, debugPlugin, frequencyPlugin } from '@prosdevlab/experience-sdk-plugins';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type Definitions for Experience SDK
|
|
5
|
+
*
|
|
6
|
+
* These types define the public API surface and should remain stable.
|
|
7
|
+
* Breaking changes to these types require a major version bump.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Experience Definition
|
|
11
|
+
*
|
|
12
|
+
* An experience represents a targeted piece of content (banner, modal, tooltip)
|
|
13
|
+
* that should be shown to users based on targeting rules.
|
|
14
|
+
*/
|
|
15
|
+
interface Experience {
|
|
16
|
+
/** Unique identifier for the experience */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Type of experience to render */
|
|
19
|
+
type: 'banner' | 'modal' | 'tooltip';
|
|
20
|
+
/** Rules that determine when/where to show this experience */
|
|
21
|
+
targeting: TargetingRules;
|
|
22
|
+
/** Content to display (type-specific) */
|
|
23
|
+
content: ExperienceContent;
|
|
24
|
+
/** Optional frequency capping configuration */
|
|
25
|
+
frequency?: FrequencyConfig;
|
|
26
|
+
/** Priority for ordering (higher = more important, default: 0) */
|
|
27
|
+
priority?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Targeting Rules
|
|
31
|
+
*
|
|
32
|
+
* Rules that determine when an experience should be shown.
|
|
33
|
+
* All rules must pass for the experience to be shown.
|
|
34
|
+
*/
|
|
35
|
+
interface TargetingRules {
|
|
36
|
+
/** URL-based targeting */
|
|
37
|
+
url?: UrlRule;
|
|
38
|
+
/** Frequency-based targeting */
|
|
39
|
+
frequency?: FrequencyRule;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* URL Targeting Rule
|
|
43
|
+
*
|
|
44
|
+
* Determines if current URL matches the target.
|
|
45
|
+
* Multiple patterns can be specified; first match wins.
|
|
46
|
+
*/
|
|
47
|
+
interface UrlRule {
|
|
48
|
+
/** URL must contain this string */
|
|
49
|
+
contains?: string;
|
|
50
|
+
/** URL must exactly match this string */
|
|
51
|
+
equals?: string;
|
|
52
|
+
/** URL must match this regular expression */
|
|
53
|
+
matches?: RegExp;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Frequency Targeting Rule
|
|
57
|
+
*
|
|
58
|
+
* Limits how often an experience can be shown.
|
|
59
|
+
*/
|
|
60
|
+
interface FrequencyRule {
|
|
61
|
+
/** Maximum number of times to show */
|
|
62
|
+
max: number;
|
|
63
|
+
/** Time period for the cap */
|
|
64
|
+
per: 'session' | 'day' | 'week';
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Frequency Configuration
|
|
68
|
+
*
|
|
69
|
+
* Configuration for frequency capping at the experience level.
|
|
70
|
+
*/
|
|
71
|
+
interface FrequencyConfig {
|
|
72
|
+
/** Maximum number of impressions */
|
|
73
|
+
max: number;
|
|
74
|
+
/** Time period for the cap */
|
|
75
|
+
per: 'session' | 'day' | 'week';
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Experience Content (type-specific)
|
|
79
|
+
*
|
|
80
|
+
* Union type for all possible experience content types.
|
|
81
|
+
*/
|
|
82
|
+
type ExperienceContent = BannerContent | ModalContent | TooltipContent;
|
|
83
|
+
/**
|
|
84
|
+
* Banner Content
|
|
85
|
+
*
|
|
86
|
+
* Content for banner-type experiences.
|
|
87
|
+
*/
|
|
88
|
+
interface BannerContent {
|
|
89
|
+
/** Banner title/heading */
|
|
90
|
+
title: string;
|
|
91
|
+
/** Banner message/body text */
|
|
92
|
+
message: string;
|
|
93
|
+
/** Whether the banner can be dismissed */
|
|
94
|
+
dismissable?: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Modal Content
|
|
98
|
+
*
|
|
99
|
+
* Content for modal-type experiences.
|
|
100
|
+
*/
|
|
101
|
+
interface ModalContent {
|
|
102
|
+
/** Modal title */
|
|
103
|
+
title: string;
|
|
104
|
+
/** Modal body content */
|
|
105
|
+
body: string;
|
|
106
|
+
/** Optional action buttons */
|
|
107
|
+
actions?: ModalAction[];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Tooltip Content
|
|
111
|
+
*
|
|
112
|
+
* Content for tooltip-type experiences.
|
|
113
|
+
*/
|
|
114
|
+
interface TooltipContent {
|
|
115
|
+
/** Tooltip text */
|
|
116
|
+
text: string;
|
|
117
|
+
/** Position relative to target element */
|
|
118
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Modal Action Button
|
|
122
|
+
*
|
|
123
|
+
* Defines an action button in a modal.
|
|
124
|
+
*/
|
|
125
|
+
interface ModalAction {
|
|
126
|
+
/** Button label text */
|
|
127
|
+
label: string;
|
|
128
|
+
/** Action to perform when clicked */
|
|
129
|
+
action: 'close' | 'confirm' | 'dismiss';
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Evaluation Context
|
|
133
|
+
*
|
|
134
|
+
* Context information used to evaluate targeting rules.
|
|
135
|
+
* This is the input to the decision-making process.
|
|
136
|
+
*/
|
|
137
|
+
interface Context {
|
|
138
|
+
/** Current page URL */
|
|
139
|
+
url?: string;
|
|
140
|
+
/** User-specific context */
|
|
141
|
+
user?: UserContext;
|
|
142
|
+
/** Evaluation timestamp */
|
|
143
|
+
timestamp?: number;
|
|
144
|
+
/** Custom context properties */
|
|
145
|
+
custom?: Record<string, any>;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* User Context
|
|
149
|
+
*
|
|
150
|
+
* User-specific information for targeting.
|
|
151
|
+
*/
|
|
152
|
+
interface UserContext {
|
|
153
|
+
/** User identifier */
|
|
154
|
+
id?: string;
|
|
155
|
+
/** Whether user is a returning visitor */
|
|
156
|
+
returning?: boolean;
|
|
157
|
+
/** Additional custom user properties */
|
|
158
|
+
[key: string]: any;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Decision Output - Core of Explainability
|
|
162
|
+
*
|
|
163
|
+
* The result of evaluating experiences against a context.
|
|
164
|
+
* Includes human-readable reasons and machine-readable trace.
|
|
165
|
+
*/
|
|
166
|
+
interface Decision {
|
|
167
|
+
/** Whether to show an experience */
|
|
168
|
+
show: boolean;
|
|
169
|
+
/** ID of the experience to show (if show=true) */
|
|
170
|
+
experienceId?: string;
|
|
171
|
+
/** Human-readable reasons for the decision */
|
|
172
|
+
reasons: string[];
|
|
173
|
+
/** Machine-readable trace of evaluation steps */
|
|
174
|
+
trace: TraceStep[];
|
|
175
|
+
/** Context used for evaluation */
|
|
176
|
+
context: Context;
|
|
177
|
+
/** Metadata about the evaluation */
|
|
178
|
+
metadata: DecisionMetadata;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Trace Step
|
|
182
|
+
*
|
|
183
|
+
* A single step in the evaluation trace.
|
|
184
|
+
* Provides detailed information about each evaluation step.
|
|
185
|
+
*/
|
|
186
|
+
interface TraceStep {
|
|
187
|
+
/** Name of the evaluation step */
|
|
188
|
+
step: string;
|
|
189
|
+
/** When this step started (unix timestamp) */
|
|
190
|
+
timestamp: number;
|
|
191
|
+
/** How long this step took (milliseconds) */
|
|
192
|
+
duration: number;
|
|
193
|
+
/** Input to this step */
|
|
194
|
+
input?: any;
|
|
195
|
+
/** Output from this step */
|
|
196
|
+
output?: any;
|
|
197
|
+
/** Whether this step passed */
|
|
198
|
+
passed: boolean;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Decision Metadata
|
|
202
|
+
*
|
|
203
|
+
* Metadata about the evaluation process.
|
|
204
|
+
*/
|
|
205
|
+
interface DecisionMetadata {
|
|
206
|
+
/** When evaluation completed (unix timestamp) */
|
|
207
|
+
evaluatedAt: number;
|
|
208
|
+
/** Total time taken (milliseconds) */
|
|
209
|
+
totalDuration: number;
|
|
210
|
+
/** Number of experiences evaluated */
|
|
211
|
+
experiencesEvaluated: number;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Experience SDK Configuration
|
|
215
|
+
*
|
|
216
|
+
* Configuration options for the Experience SDK.
|
|
217
|
+
*/
|
|
218
|
+
interface ExperienceConfig {
|
|
219
|
+
/** Enable debug mode (verbose logging) */
|
|
220
|
+
debug?: boolean;
|
|
221
|
+
/** Storage backend to use */
|
|
222
|
+
storage?: 'session' | 'local' | 'memory';
|
|
223
|
+
/** Additional custom configuration */
|
|
224
|
+
[key: string]: any;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Runtime State
|
|
228
|
+
*
|
|
229
|
+
* Internal runtime state (exposed for inspection/debugging).
|
|
230
|
+
*/
|
|
231
|
+
interface RuntimeState {
|
|
232
|
+
/** Whether the runtime has been initialized */
|
|
233
|
+
initialized: boolean;
|
|
234
|
+
/** Registered experiences */
|
|
235
|
+
experiences: Map<string, Experience>;
|
|
236
|
+
/** History of decisions made */
|
|
237
|
+
decisions: Decision[];
|
|
238
|
+
/** Current configuration */
|
|
239
|
+
config: ExperienceConfig;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Experience Runtime
|
|
244
|
+
*
|
|
245
|
+
* Core class that manages experience registration and evaluation.
|
|
246
|
+
* Built on @lytics/sdk-kit for plugin system and lifecycle management.
|
|
247
|
+
*
|
|
248
|
+
* Design principles:
|
|
249
|
+
* - Pure functions for evaluation logic (easy to test)
|
|
250
|
+
* - Event-driven architecture (extensible via plugins)
|
|
251
|
+
* - Explainability-first (every decision has reasons)
|
|
252
|
+
*/
|
|
253
|
+
declare class ExperienceRuntime {
|
|
254
|
+
private sdk;
|
|
255
|
+
private experiences;
|
|
256
|
+
private decisions;
|
|
257
|
+
private initialized;
|
|
258
|
+
private destroyed;
|
|
259
|
+
constructor(config?: ExperienceConfig);
|
|
260
|
+
/**
|
|
261
|
+
* Initialize the runtime
|
|
262
|
+
*/
|
|
263
|
+
init(config?: ExperienceConfig): Promise<void>;
|
|
264
|
+
/**
|
|
265
|
+
* Register an experience
|
|
266
|
+
*/
|
|
267
|
+
register(id: string, experience: Omit<Experience, 'id'>): void;
|
|
268
|
+
/**
|
|
269
|
+
* Evaluate experiences against context
|
|
270
|
+
* Returns decision with explainability
|
|
271
|
+
* First match wins (use evaluateAll() for multiple experiences)
|
|
272
|
+
*/
|
|
273
|
+
evaluate(context?: Partial<Context>): Decision;
|
|
274
|
+
/**
|
|
275
|
+
* Evaluate all experiences against context
|
|
276
|
+
* Returns multiple decisions (sorted by priority)
|
|
277
|
+
* All matching experiences will be shown
|
|
278
|
+
*/
|
|
279
|
+
evaluateAll(context?: Partial<Context>): Decision[];
|
|
280
|
+
/**
|
|
281
|
+
* Explain a specific experience
|
|
282
|
+
*/
|
|
283
|
+
explain(experienceId: string): Decision | null;
|
|
284
|
+
/**
|
|
285
|
+
* Get runtime state (for inspection)
|
|
286
|
+
*/
|
|
287
|
+
getState(): RuntimeState;
|
|
288
|
+
/**
|
|
289
|
+
* Event subscription (proxy to SDK)
|
|
290
|
+
*/
|
|
291
|
+
on(event: string, handler: (...args: any[]) => void): () => void;
|
|
292
|
+
/**
|
|
293
|
+
* Destroy runtime
|
|
294
|
+
*/
|
|
295
|
+
destroy(): Promise<void>;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Build evaluation context from partial input
|
|
299
|
+
* Pure function - no side effects
|
|
300
|
+
*/
|
|
301
|
+
declare function buildContext(partial?: Partial<Context>): Context;
|
|
302
|
+
/**
|
|
303
|
+
* Evaluate an experience against context
|
|
304
|
+
* Pure function - returns reasons and trace
|
|
305
|
+
*/
|
|
306
|
+
declare function evaluateExperience(experience: Experience, context: Context): {
|
|
307
|
+
matched: boolean;
|
|
308
|
+
reasons: string[];
|
|
309
|
+
trace: TraceStep[];
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* Evaluate URL targeting rule
|
|
313
|
+
* Pure function - deterministic output
|
|
314
|
+
*/
|
|
315
|
+
declare function evaluateUrlRule(rule: UrlRule, url?: string): boolean;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Singleton Pattern Implementation
|
|
319
|
+
*
|
|
320
|
+
* Provides a default singleton instance with convenient wrapper functions
|
|
321
|
+
* for simple use cases, plus createInstance() for advanced scenarios.
|
|
322
|
+
*/
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Create a new Experience SDK instance
|
|
326
|
+
*
|
|
327
|
+
* Use this for advanced scenarios where you need multiple isolated runtimes.
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* ```typescript
|
|
331
|
+
* import { createInstance } from '@prosdevlab/experience-sdk';
|
|
332
|
+
*
|
|
333
|
+
* const exp = createInstance({ debug: true });
|
|
334
|
+
* await exp.init();
|
|
335
|
+
* exp.register('welcome', { ... });
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
declare function createInstance(config?: ExperienceConfig): ExperienceRuntime;
|
|
339
|
+
/**
|
|
340
|
+
* Initialize the Experience SDK
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```typescript
|
|
344
|
+
* import { init } from '@prosdevlab/experience-sdk';
|
|
345
|
+
* await init({ debug: true });
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
declare function init(config?: ExperienceConfig): Promise<void>;
|
|
349
|
+
/**
|
|
350
|
+
* Register an experience
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```typescript
|
|
354
|
+
* import { register } from '@prosdevlab/experience-sdk';
|
|
355
|
+
*
|
|
356
|
+
* register('welcome-banner', {
|
|
357
|
+
* type: 'banner',
|
|
358
|
+
* targeting: { url: { contains: '/' } },
|
|
359
|
+
* content: { title: 'Welcome!', message: 'Thanks for visiting' }
|
|
360
|
+
* });
|
|
361
|
+
* ```
|
|
362
|
+
*/
|
|
363
|
+
declare function register(id: string, experience: Omit<Experience, 'id'>): void;
|
|
364
|
+
/**
|
|
365
|
+
* Evaluate experiences against current context
|
|
366
|
+
* First match wins (use evaluateAll() for multiple experiences)
|
|
367
|
+
*
|
|
368
|
+
* @example
|
|
369
|
+
* ```typescript
|
|
370
|
+
* import { evaluate } from '@prosdevlab/experience-sdk';
|
|
371
|
+
*
|
|
372
|
+
* const decision = evaluate({ url: window.location.href });
|
|
373
|
+
* if (decision.show) {
|
|
374
|
+
* console.log('Show experience:', decision.experienceId);
|
|
375
|
+
* console.log('Reasons:', decision.reasons);
|
|
376
|
+
* }
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
declare function evaluate(context?: Partial<Context>): Decision;
|
|
380
|
+
/**
|
|
381
|
+
* Evaluate all experiences against current context
|
|
382
|
+
* Returns array of decisions sorted by priority (higher = more important)
|
|
383
|
+
* All matching experiences will be shown
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```typescript
|
|
387
|
+
* import { evaluateAll } from '@prosdevlab/experience-sdk';
|
|
388
|
+
*
|
|
389
|
+
* const decisions = evaluateAll({ url: window.location.href });
|
|
390
|
+
* decisions.forEach(decision => {
|
|
391
|
+
* if (decision.show) {
|
|
392
|
+
* console.log('Show:', decision.experienceId);
|
|
393
|
+
* console.log('Reasons:', decision.reasons);
|
|
394
|
+
* }
|
|
395
|
+
* });
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
398
|
+
declare function evaluateAll(context?: Partial<Context>): Decision[];
|
|
399
|
+
/**
|
|
400
|
+
* Explain why a specific experience would/wouldn't show
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```typescript
|
|
404
|
+
* import { explain } from '@prosdevlab/experience-sdk';
|
|
405
|
+
*
|
|
406
|
+
* const explanation = explain('welcome-banner');
|
|
407
|
+
* console.log('Would show?', explanation?.show);
|
|
408
|
+
* console.log('Reasons:', explanation?.reasons);
|
|
409
|
+
* ```
|
|
410
|
+
*/
|
|
411
|
+
declare function explain(experienceId: string): Decision | null;
|
|
412
|
+
/**
|
|
413
|
+
* Get current runtime state
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```typescript
|
|
417
|
+
* import { getState } from '@prosdevlab/experience-sdk';
|
|
418
|
+
*
|
|
419
|
+
* const state = getState();
|
|
420
|
+
* console.log('Initialized?', state.initialized);
|
|
421
|
+
* console.log('Experiences:', Array.from(state.experiences.keys()));
|
|
422
|
+
* ```
|
|
423
|
+
*/
|
|
424
|
+
declare function getState(): RuntimeState;
|
|
425
|
+
/**
|
|
426
|
+
* Subscribe to SDK events
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```typescript
|
|
430
|
+
* import { on } from '@prosdevlab/experience-sdk';
|
|
431
|
+
*
|
|
432
|
+
* const unsubscribe = on('experiences:evaluated', (decision) => {
|
|
433
|
+
* console.log('Evaluation:', decision);
|
|
434
|
+
* });
|
|
435
|
+
*
|
|
436
|
+
* // Later: unsubscribe()
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
439
|
+
declare function on(event: string, handler: (...args: unknown[]) => void): () => void;
|
|
440
|
+
/**
|
|
441
|
+
* Destroy the SDK instance
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* ```typescript
|
|
445
|
+
* import { destroy } from '@prosdevlab/experience-sdk';
|
|
446
|
+
* await destroy();
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
declare function destroy(): Promise<void>;
|
|
450
|
+
|
|
451
|
+
export { type BannerContent, type Context, type Decision, type DecisionMetadata, type Experience, type ExperienceConfig, type ExperienceContent, ExperienceRuntime, type FrequencyConfig, type FrequencyRule, type ModalAction, type ModalContent, type RuntimeState, type TargetingRules, type TooltipContent, type TraceStep, type UrlRule, type UserContext, buildContext, createInstance, destroy, evaluate, evaluateAll, evaluateExperience, evaluateUrlRule, explain, getState, init, on, register };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
var experiences=(function(exports){'use strict';var A=class{storage=new Map;get(e){return this.storage.get(e)??null}set(e,t,r){this.storage.set(e,t);}remove(e){this.storage.delete(e);}clear(){this.storage.clear();}isSupported(){return true}},Z=class{fallback=null;defaultOptions;constructor(e={}){this.defaultOptions={path:"/",sameSite:"lax",...e};}get(e){if(!this.isSupported())return this.getFallback().get(e);try{let t=`${encodeURIComponent(e)}=`,r=document.cookie.split(";");for(let n of r)if(n=n.trim(),n.startsWith(t))return decodeURIComponent(n.substring(t.length));return null}catch(t){return console.warn("Cookie get failed:",t),this.getFallback().get(e)}}set(e,t,r){if(!this.isSupported()){this.getFallback().set(e,t);return}try{let n={...this.defaultOptions,...r},a=[`${encodeURIComponent(e)}=${encodeURIComponent(t)}`];if(n.ttl){let f=new Date;f.setTime(f.getTime()+n.ttl*1e3),a.push(`expires=${f.toUTCString()}`);}n.path&&a.push(`path=${n.path}`),n.domain&&a.push(`domain=${n.domain}`),n.secure&&a.push("secure"),n.sameSite&&a.push(`samesite=${n.sameSite}`),document.cookie=a.join("; ");}catch(n){console.warn("Cookie set failed, falling back to memory:",n),this.getFallback().set(e,t);}}remove(e){if(!this.isSupported()){this.getFallback().remove(e);return}try{let t=this.defaultOptions,r=[`${encodeURIComponent(e)}=`,"expires=Thu, 01 Jan 1970 00:00:00 UTC"];t.path&&r.push(`path=${t.path}`),t.domain&&r.push(`domain=${t.domain}`),document.cookie=r.join("; ");}catch(t){console.warn("Cookie remove failed:",t),this.getFallback().remove(e);}}clear(){if(!this.isSupported()){this.getFallback().clear();return}try{let e=document.cookie.split(";");for(let t of e){t=t.trim();let r=t.indexOf("="),n=r>-1?t.substring(0,r):t;this.remove(decodeURIComponent(n));}}catch(e){console.warn("Cookie clear failed:",e),this.getFallback().clear();}}isSupported(){try{if(typeof document>"u"||!document.cookie)return !1;let e="__cookie_test__";document.cookie=`${e}=test; path=/`;let t=document.cookie.indexOf(e)!==-1;return document.cookie=`${e}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`,t}catch{return false}}getFallback(){return this.fallback||(this.fallback=new A),this.fallback}},ee=class{fallback=null;get(e){if(!this.isSupported())return this.getFallback().get(e);try{return localStorage.getItem(e)}catch(t){return console.warn("localStorage.getItem failed:",t),this.getFallback().get(e)}}set(e,t,r){if(!this.isSupported()){this.getFallback().set(e,t);return}try{localStorage.setItem(e,t);}catch(n){console.warn("localStorage.setItem failed, falling back to memory:",n),this.getFallback().set(e,t);}}remove(e){if(!this.isSupported()){this.getFallback().remove(e);return}try{localStorage.removeItem(e);}catch(t){console.warn("localStorage.removeItem failed:",t),this.getFallback().remove(e);}}clear(){if(!this.isSupported()){this.getFallback().clear();return}try{localStorage.clear();}catch(e){console.warn("localStorage.clear failed:",e),this.getFallback().clear();}}isSupported(){try{let e="__storage_test__";return localStorage.setItem(e,"test"),localStorage.removeItem(e),!0}catch{return false}}getFallback(){return this.fallback||(this.fallback=new A),this.fallback}},te=class{fallback=null;get(e){if(!this.isSupported())return this.getFallback().get(e);try{return sessionStorage.getItem(e)}catch(t){return console.warn("sessionStorage.getItem failed:",t),this.getFallback().get(e)}}set(e,t,r){if(!this.isSupported()){this.getFallback().set(e,t);return}try{sessionStorage.setItem(e,t);}catch(n){console.warn("sessionStorage.setItem failed, falling back to memory:",n),this.getFallback().set(e,t);}}remove(e){if(!this.isSupported()){this.getFallback().remove(e);return}try{sessionStorage.removeItem(e);}catch(t){console.warn("sessionStorage.removeItem failed:",t),this.getFallback().remove(e);}}clear(){if(!this.isSupported()){this.getFallback().clear();return}try{sessionStorage.clear();}catch(e){console.warn("sessionStorage.clear failed:",e),this.getFallback().clear();}}isSupported(){try{let e="__storage_test__";return sessionStorage.setItem(e,"test"),sessionStorage.removeItem(e),!0}catch{return false}}getFallback(){return this.fallback||(this.fallback=new A),this.fallback}},D=(e,t,r)=>{e.ns("storage"),e.defaults({storage:{backend:"localStorage",namespace:"",path:"/",sameSite:"lax"}});let n=()=>r.get("storage.backend")??"localStorage",a=()=>r.get("storage.namespace")??"",f=()=>r.get("storage.ttl"),y=()=>({domain:r.get("storage.domain"),path:r.get("storage.path")??"/",secure:r.get("storage.secure"),sameSite:r.get("storage.sameSite")??"lax"}),i={};function s(o){if(!i[o])switch(o){case "localStorage":i[o]=new ee;break;case "sessionStorage":i[o]=new te;break;case "cookie":i[o]=new Z(y());break;case "memory":i[o]=new A;break}return i[o]}function l(o,c){let u=c??a();return u?`${u}:${o}`:o}function b(o){return o.expires?Date.now()>o.expires:false}function x(o,c,u){let h=u?.backend??n(),w=s(h),S=l(o,u?.namespace),k={value:c},C=u?.ttl??f();C&&(k.expires=Date.now()+C*1e3);let d=JSON.stringify(k);w.set(S,d,u),e.emit("storage:set",{key:o,value:c,backend:h});}function I(o,c){let u=c?.backend??n(),h=s(u),w=l(o,c?.namespace),S=h.get(w);if(!S)return null;try{let k=JSON.parse(S);return b(k)?(h.remove(w),e.emit("storage:expired",{key:o,backend:u}),null):(e.emit("storage:get",{key:o,backend:u}),k.value)}catch(k){return console.warn("Failed to parse stored value:",k),h.remove(w),null}}function m(o,c){let u=c?.backend??n(),h=s(u),w=l(o,c?.namespace);h.remove(w),e.emit("storage:remove",{key:o,backend:u});}function p(o){let c=o?.backend??n(),u=s(c),h=o?.namespace;if(!h){u.clear(),e.emit("storage:clear",{backend:c});return}if(c==="localStorage"||c==="sessionStorage"){let w=c==="localStorage"?localStorage:sessionStorage,S=`${h}:`,k=[];for(let C=0;C<w.length;C++){let d=w.key(C);d?.startsWith(S)&&k.push(d);}for(let C of k)u.remove(C);}else if(c==="cookie"){let w=`${h}:`,S=document.cookie.split(";");for(let k of S){let C=k.trim(),d=C.indexOf("=");if(d===-1)continue;let v=C.substring(0,d),E=decodeURIComponent(v);E.startsWith(w)&&u.remove(E);}}else u.clear();e.emit("storage:clear",{backend:c,namespace:h});}function g(o){let c=o??n();return s(c).isSupported()}e.expose({storage:{set:x,get:I,remove:m,clear:p,isSupported:g}}),t.on("sdk:ready",()=>{let o=n();s(o).isSupported()||console.warn(`Storage backend '${o}' is not supported, falling back to memory storage`);});};var F=(e,t,r)=>{e.ns("banner"),e.defaults({banner:{position:"top",dismissable:true,zIndex:1e4}});let n=new Map;function a(s){let l=s.content,b=l.position??r.get("banner.position")??"top",x=l.dismissable??r.get("banner.dismissable")??true,I=r.get("banner.zIndex")??1e4,m=document.documentElement.classList.contains("dark"),p=m?"#1f2937":"#f9fafb",g=m?"#f3f4f6":"#111827",o=m?"#374151":"#e5e7eb",c=m?"rgba(0, 0, 0, 0.3)":"rgba(0, 0, 0, 0.05)",u=document.createElement("div");u.setAttribute("data-experience-id",s.id);let h=`banner-responsive-${s.id}`;if(!document.getElementById(h)){let d=document.createElement("style");d.id=h,d.textContent=`
|
|
2
|
+
@media (max-width: 640px) {
|
|
3
|
+
[data-experience-id="${s.id}"] {
|
|
4
|
+
flex-direction: column !important;
|
|
5
|
+
align-items: flex-start !important;
|
|
6
|
+
}
|
|
7
|
+
[data-experience-id="${s.id}"] > div:last-child {
|
|
8
|
+
width: 100%;
|
|
9
|
+
flex-direction: column !important;
|
|
10
|
+
}
|
|
11
|
+
[data-experience-id="${s.id}"] button {
|
|
12
|
+
width: 100%;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
`,document.head.appendChild(d);}u.style.cssText=`
|
|
16
|
+
position: fixed;
|
|
17
|
+
${b}: 0;
|
|
18
|
+
left: 0;
|
|
19
|
+
right: 0;
|
|
20
|
+
background: ${p};
|
|
21
|
+
color: ${g};
|
|
22
|
+
padding: 16px 20px;
|
|
23
|
+
border-${b==="top"?"bottom":"top"}: 1px solid ${o};
|
|
24
|
+
box-shadow: 0 ${b==="top"?"1":"-1"}px 3px 0 ${c};
|
|
25
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
26
|
+
font-size: 14px;
|
|
27
|
+
line-height: 1.5;
|
|
28
|
+
z-index: ${I};
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: space-between;
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
`;let w=document.createElement("div");if(w.style.cssText="flex: 1; margin-right: 20px;",l.title){let d=document.createElement("div");d.textContent=l.title,d.style.cssText="font-weight: 600; margin-bottom: 4px;",w.appendChild(d);}let S=document.createElement("div");S.textContent=l.message,w.appendChild(S),u.appendChild(w);let k=document.createElement("div");k.style.cssText=`
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 12px;
|
|
37
|
+
flex-wrap: wrap;
|
|
38
|
+
`;function C(d){let v=document.createElement("button");v.textContent=d.text;let E=d.variant||"primary",q,R,O,$;return E==="primary"?(q=m?"#3b82f6":"#2563eb",R=m?"#2563eb":"#1d4ed8",O="#ffffff",$="none"):E==="secondary"?(q=m?"#374151":"#ffffff",R=m?"#4b5563":"#f9fafb",O=m?"#f3f4f6":"#374151",$=m?"1px solid #4b5563":"1px solid #d1d5db"):(q="transparent",R=m?"rgba(255, 255, 255, 0.1)":"rgba(0, 0, 0, 0.05)",O=m?"#93c5fd":"#2563eb",$="none"),v.style.cssText=`
|
|
39
|
+
background: ${q};
|
|
40
|
+
border: ${$};
|
|
41
|
+
color: ${O};
|
|
42
|
+
padding: ${E==="link"?"4px 8px":"8px 16px"};
|
|
43
|
+
font-size: 14px;
|
|
44
|
+
font-weight: ${E==="link"?"400":"500"};
|
|
45
|
+
border-radius: 6px;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
transition: all 0.2s;
|
|
48
|
+
text-decoration: ${E==="link"?"underline":"none"};
|
|
49
|
+
`,v.addEventListener("mouseenter",()=>{v.style.background=R;}),v.addEventListener("mouseleave",()=>{v.style.background=q;}),v.addEventListener("click",()=>{t.emit("experiences:action",{experienceId:s.id,type:"banner",action:d.action,url:d.url,metadata:d.metadata,variant:E,timestamp:Date.now()}),d.url&&(window.location.href=d.url);}),v}if(l.buttons&&l.buttons.length>0&&l.buttons.forEach(d=>{let v=C(d);k.appendChild(v);}),x){let d=document.createElement("button");d.innerHTML="×",d.setAttribute("aria-label","Close banner");let v=m?"#9ca3af":"#6b7280";d.style.cssText=`
|
|
50
|
+
background: transparent;
|
|
51
|
+
border: none;
|
|
52
|
+
color: ${v};
|
|
53
|
+
font-size: 24px;
|
|
54
|
+
line-height: 1;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
padding: 0;
|
|
57
|
+
margin: 0;
|
|
58
|
+
opacity: 0.7;
|
|
59
|
+
transition: opacity 0.2s;
|
|
60
|
+
`,d.addEventListener("mouseenter",()=>{d.style.opacity="1";}),d.addEventListener("mouseleave",()=>{d.style.opacity="0.7";}),d.addEventListener("click",()=>{y(s.id),t.emit("experiences:dismissed",{experienceId:s.id,type:"banner"});}),k.appendChild(d);}return u.appendChild(k),u}function f(s){if(n.has(s.id)||typeof document>"u")return;let l=a(s);document.body.appendChild(l),n.set(s.id,l),t.emit("experiences:shown",{experienceId:s.id,type:"banner",timestamp:Date.now()});}function y(s){if(s){let l=n.get(s);l?.parentNode&&l.parentNode.removeChild(l),n.delete(s);}else for(let[l,b]of n.entries())b?.parentNode&&b.parentNode.removeChild(b),n.delete(l);}function i(){return n.size>0}e.expose({banner:{show:f,remove:y,isShowing:i}}),t.on("experiences:evaluated",s=>{let l=Array.isArray(s)?s:[s];for(let b of l){let x=b,I=x.decision,m=x.experience;m?.type==="banner"&&(I?.show?f(m):m.id&&n.has(m.id)&&y(m.id));}}),t.on("sdk:destroy",()=>{y();});},_=(e,t,r)=>{e.ns("debug"),e.defaults({debug:{enabled:false,console:false,window:true}});let n=()=>r.get("debug.enabled")??false,a=()=>r.get("debug.console")??false,f=()=>r.get("debug.window")??true,y=(i,s)=>{if(!n())return;let b={timestamp:new Date().toISOString(),message:i,data:s};if(a()&&console.log(`[experiences] ${i}`,s||""),f()&&typeof window<"u"){let x=new CustomEvent("experience-sdk:debug",{detail:b});window.dispatchEvent(x);}};e.expose({debug:{log:y,isEnabled:n}}),n()&&(t.on("experiences:ready",()=>{n()&&y("SDK initialized and ready");}),t.on("experiences:registered",i=>{n()&&y("Experience registered",i);}),t.on("experiences:evaluated",i=>{n()&&y("Experience evaluated",i);}));},z=(e,t,r)=>{e.ns("frequency"),e.defaults({frequency:{enabled:true,namespace:"experiences:frequency"}});let n=new Map;t.storage||t.use(D);let a=()=>r.get("frequency.enabled")??true,f=()=>r.get("frequency.namespace")??"experiences:frequency",y=p=>p==="session"?sessionStorage:localStorage,i=p=>`${f()}:${p}`,s=(p,g)=>{let o=y(g),c=i(p),u=o.getItem(c);if(!u)return {count:0,lastImpression:0,impressions:[],per:g};try{return JSON.parse(u)}catch{return {count:0,lastImpression:0,impressions:[],per:g}}},l=(p,g)=>{let o=g.per||"session",c=y(o),u=i(p);c.setItem(u,JSON.stringify(g));},b=p=>{switch(p){case "session":return Number.POSITIVE_INFINITY;case "day":return 1440*60*1e3;case "week":return 10080*60*1e3}},x=(p,g="session")=>a()?s(p,g).count:0,I=(p,g,o)=>{if(!a())return false;let c=s(p,o),u=b(o),h=Date.now();return o==="session"?c.count>=g:c.impressions.filter(S=>h-S<u).length>=g},m=(p,g="session")=>{if(!a())return;let o=s(p,g),c=Date.now();o.count+=1,o.lastImpression=c,o.impressions.push(c),o.per=g;let u=c-10080*60*1e3;o.impressions=o.impressions.filter(h=>h>u),l(p,o),t.emit("experiences:impression-recorded",{experienceId:p,count:o.count,timestamp:c});};e.expose({frequency:{getImpressionCount:x,hasReachedCap:I,recordImpression:m,_registerExperience:(p,g)=>{n.set(p,g);}}}),a()&&t.on("experiences:evaluated",p=>{let g=Array.isArray(p)?p:[p];for(let o of g){let c=o.decision;if(c?.show&&c.experienceId){let u=n.get(c.experienceId)||"session";if(!n.has(c.experienceId)){let h=c.trace.find(w=>w.step==="check-frequency-cap");h?.input&&typeof h.input=="object"&&"per"in h.input&&(u=h.input.per,n.set(c.experienceId,u));}m(c.experienceId,u);}}});};function j(e,t){let r={...e};for(let n in t){if(!Object.hasOwn(t,n))continue;let a=t[n],f=r[n];if(f===void 0){r[n]=a;continue}B(f)&&B(a)&&(r[n]=j(f,a));}return r}function B(e){return e===null||typeof e!="object"?false:Object.prototype.toString.call(e)==="[object Object]"}var ne=class{data={};required=new Set;constructor(e={}){this.data=JSON.parse(JSON.stringify(e));}defaults(e){this.data=j(this.data,e);}merge(e){this.data=j(e,this.data);}get(e){let t=e.split("."),r=this.data;for(let n of t){if(r==null)return;r=r[n];}return r}set(e,t){let r=e.split("."),n=r.pop();if(!n)return;let a=this.data;for(let f of r)(a[f]==null||typeof a[f]!="object")&&(a[f]={}),a=a[f];a[n]=t;}markRequired(e){e!=null&&this.required.add(e);}isRequired(e){return this.required.has(e)}getAll(){return JSON.parse(JSON.stringify(this.data))}},re=class{subscriptions=[];on(e,t){if(typeof t!="function")throw new TypeError("handler must be a function");let r={pattern:e,compiledPattern:this.compilePattern(e),handler:t};return this.subscriptions.push(r),()=>this.off(e,t)}off(e,t){this.subscriptions=this.subscriptions.filter(r=>!(r.pattern===e&&r.handler===t));}emit(e,...t){for(let r of this.subscriptions)if(r.compiledPattern.test(e))try{r.handler(...t);}catch(n){console.error(`Error in event handler for "${e}":`,n);}}removeAllListeners(){this.subscriptions=[];}compilePattern(e){let r=this.escapeRegExp(e).replace(/\\\*/g,"(.*?)");return new RegExp(`^${r}$`)}escapeRegExp(e){return e.replace(/[\\^$*+?.()|[\]{}]/g,"\\$&")}},se=class{sdk;constructor(e){this.sdk=e;}expose(e){Object.assign(this.sdk,e);}},oe=class{name="";ns(e){if(this.name)throw new Error(`Namespace already set to "${this.name}". Cannot reassign to "${e}".`);this.name=e;}},N=class{configInstance;emitter;plugins=new Map;isInitialized=false;constructor(e={}){this.emitter=new re,this.configInstance=new ne(e);}use(e){let t=new oe,r=new se(this),n={ns:t.ns.bind(t),defaults:this.configInstance.defaults.bind(this.configInstance),on:this.emitter.on.bind(this.emitter),off:this.emitter.off.bind(this.emitter),emit:this.emitter.emit.bind(this.emitter),expose:r.expose.bind(r),mustEnable:()=>{t.name&&this.configInstance.markRequired(t.name);}};return e(n,this,this.configInstance),t.name&&this.plugins.set(t.name,{namespace:t,plugin:n}),this}async init(e){if(this.isInitialized){console.warn("SDK already initialized");return}e&&this.configInstance.merge(e),this.emitter.emit("sdk:init");let t=Array.from(this.plugins.keys()).filter(r=>this.configInstance.isRequired(r));for(let r of t)if(!this.plugins.has(r))throw new Error(`Required plugin "${r}" is not registered`);this.isInitialized=true,this.emitter.emit("sdk:ready");}async destroy(){this.emitter.emit("sdk:destroy"),this.plugins.clear(),this.emitter.removeAllListeners(),this.isInitialized=false;}get(e){return this.configInstance.get(e)}set(e,t){this.configInstance.set(e,t);}on(e,t){return this.emitter.on(e,t)}off(e,t){this.emitter.off(e,t);}emit(e,...t){this.emitter.emit(e,...t);}getAll(){return this.configInstance.getAll()}isReady(){return this.isInitialized}};var P=class{sdk;experiences=new Map;decisions=[];initialized=false;destroyed=false;constructor(t={}){this.sdk=new N({name:"experience-sdk",...t}),this.sdk.use(D),this.sdk.use(_),this.sdk.use(z),this.sdk.use(F);}async init(t){if(this.initialized){console.warn("[experiences] Already initialized");return}this.destroyed&&(this.sdk=new N({name:"experience-sdk",...t}),this.sdk.use(D),this.sdk.use(_),this.sdk.use(z),this.sdk.use(F),this.destroyed=false),t&&Object.entries(t).forEach(([r,n])=>{this.sdk.set(r,n);}),await this.sdk.init(),this.initialized=true,this.sdk.emit("experiences:ready");}register(t,r){let n={id:t,...r};this.experiences.set(t,n),n.frequency&&this.sdk.frequency?._registerExperience&&this.sdk.frequency._registerExperience(t,n.frequency.per),this.sdk.emit("experiences:registered",{id:t,experience:n});}evaluate(t){let r=Date.now(),n=L(t),a,f=[],y=[];for(let[,s]of this.experiences){let l=U(s,n);if(f.push(...l.reasons),y.push(...l.trace),l.matched){if(s.frequency&&this.sdk.frequency){let b=Date.now(),x=this.sdk.frequency.hasReachedCap(s.id,s.frequency.max,s.frequency.per);if(y.push({step:"check-frequency-cap",timestamp:b,duration:Date.now()-b,input:s.frequency,output:x,passed:!x}),x){let m=this.sdk.frequency.getImpressionCount(s.id,s.frequency.per);f.push(`Frequency cap reached (${m}/${s.frequency.max} this ${s.frequency.per})`);continue}let I=this.sdk.frequency.getImpressionCount(s.id,s.frequency.per);f.push(`Frequency cap not reached (${I}/${s.frequency.max} this ${s.frequency.per})`);}a=s;break}}let i={show:!!a,experienceId:a?.id,reasons:f,trace:y,context:n,metadata:{evaluatedAt:Date.now(),totalDuration:Date.now()-r,experiencesEvaluated:this.experiences.size}};return this.decisions.push(i),this.sdk.emit("experiences:evaluated",{decision:i,experience:a}),i}evaluateAll(t){let r=L(t),n=Array.from(this.experiences.values()).sort((i,s)=>{let l=i.priority??0;return (s.priority??0)-l}),a=[];for(let i of n){let s=Date.now(),l=U(i,r),b=l.matched,x=[...l.reasons],I=[...l.trace];if(b&&i.frequency&&this.sdk.frequency){let p=Date.now(),g=this.sdk.frequency.hasReachedCap(i.id,i.frequency.max,i.frequency.per);if(I.push({step:"check-frequency-cap",timestamp:p,duration:Date.now()-p,input:i.frequency,output:g,passed:!g}),g){let o=this.sdk.frequency.getImpressionCount(i.id,i.frequency.per);x.push(`Frequency cap reached (${o}/${i.frequency.max} this ${i.frequency.per})`),b=false;}else {let o=this.sdk.frequency.getImpressionCount(i.id,i.frequency.per);x.push(`Frequency cap not reached (${o}/${i.frequency.max} this ${i.frequency.per})`);}}let m={show:b,experienceId:i.id,reasons:x,trace:I,context:r,metadata:{evaluatedAt:Date.now(),totalDuration:Date.now()-s,experiencesEvaluated:1}};a.push(m),this.decisions.push(m);}let f=a.filter(i=>i.show),y=f.map(i=>i.experienceId&&this.experiences.get(i.experienceId)).filter(i=>i!==void 0);return this.sdk.emit("experiences:evaluated",f.map((i,s)=>({decision:i,experience:y[s]}))),a}explain(t){let r=this.experiences.get(t);if(!r)return null;let n=L(),a=U(r,n);return {show:a.matched,experienceId:t,reasons:a.reasons,trace:a.trace,context:n,metadata:{evaluatedAt:Date.now(),totalDuration:0,experiencesEvaluated:1}}}getState(){return {initialized:this.initialized,experiences:new Map(this.experiences),decisions:[...this.decisions],config:this.sdk?this.sdk.getAll():{}}}on(t,r){return this.sdk.on(t,r)}async destroy(){this.sdk&&await this.sdk.destroy(),this.destroyed=true,this.experiences.clear(),this.decisions=[],this.initialized=false;}};function L(e){return {url:e?.url??(typeof window<"u"?window.location.href:""),timestamp:Date.now(),user:e?.user,custom:e?.custom}}function U(e,t){let r=[],n=[],a=true;if(e.targeting.url){let f=Date.now(),y=K(e.targeting.url,t.url);n.push({step:"evaluate-url-rule",timestamp:f,duration:Date.now()-f,input:{rule:e.targeting.url,url:t.url},output:y,passed:y}),y?r.push("URL matches targeting rule"):(r.push("URL does not match targeting rule"),a=false);}return {matched:a,reasons:r,trace:n}}function K(e,t=""){return e.equals!==void 0?t===e.equals:e.contains!==void 0?t.includes(e.contains):e.matches!==void 0?e.matches.test(t):true}function M(e){return new P(e)}var T=M();async function V(e){return T.init(e)}function J(e,t){T.register(e,t);}function Q(e){return T.evaluate(e)}function W(e){return T.evaluateAll(e)}function H(e){return T.explain(e)}function G(){return T.getState()}function X(e,t){return T.on(e,t)}async function Y(){return T.destroy()}var ie={createInstance:M,init:V,register:J,evaluate:Q,evaluateAll:W,explain:H,getState:G,on:X,destroy:Y};typeof window<"u"&&(window.experiences=ie);exports.ExperienceRuntime=P;exports.bannerPlugin=F;exports.buildContext=L;exports.createInstance=M;exports.debugPlugin=_;exports.destroy=Y;exports.evaluate=Q;exports.evaluateAll=W;exports.evaluateExperience=U;exports.evaluateUrlRule=K;exports.explain=H;exports.frequencyPlugin=z;exports.getState=G;exports.init=V;exports.on=X;exports.register=J;return exports;})({});//# sourceMappingURL=experience-sdk.global.js.map
|
|
61
|
+
//# sourceMappingURL=experience-sdk.global.js.map
|