@umituz/web-cloudflare 1.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/LICENSE +21 -0
- package/README.md +621 -0
- package/package.json +87 -0
- package/src/config/patterns.ts +469 -0
- package/src/config/types.ts +648 -0
- package/src/domain/entities/analytics.entity.ts +47 -0
- package/src/domain/entities/d1.entity.ts +37 -0
- package/src/domain/entities/image.entity.ts +48 -0
- package/src/domain/entities/index.ts +11 -0
- package/src/domain/entities/kv.entity.ts +34 -0
- package/src/domain/entities/r2.entity.ts +55 -0
- package/src/domain/entities/worker.entity.ts +35 -0
- package/src/domain/index.ts +7 -0
- package/src/domain/interfaces/index.ts +6 -0
- package/src/domain/interfaces/services.interface.ts +82 -0
- package/src/index.ts +53 -0
- package/src/infrastructure/constants/index.ts +13 -0
- package/src/infrastructure/domain/ai-gateway.entity.ts +169 -0
- package/src/infrastructure/domain/workflows.entity.ts +108 -0
- package/src/infrastructure/middleware/index.ts +405 -0
- package/src/infrastructure/router/index.ts +549 -0
- package/src/infrastructure/services/ai-gateway/index.ts +416 -0
- package/src/infrastructure/services/analytics/analytics.service.ts +189 -0
- package/src/infrastructure/services/analytics/index.ts +7 -0
- package/src/infrastructure/services/d1/d1.service.ts +191 -0
- package/src/infrastructure/services/d1/index.ts +7 -0
- package/src/infrastructure/services/images/images.service.ts +227 -0
- package/src/infrastructure/services/images/index.ts +7 -0
- package/src/infrastructure/services/kv/index.ts +7 -0
- package/src/infrastructure/services/kv/kv.service.ts +116 -0
- package/src/infrastructure/services/r2/index.ts +7 -0
- package/src/infrastructure/services/r2/r2.service.ts +164 -0
- package/src/infrastructure/services/workers/index.ts +7 -0
- package/src/infrastructure/services/workers/workers.service.ts +164 -0
- package/src/infrastructure/services/workflows/index.ts +437 -0
- package/src/infrastructure/utils/helpers.ts +732 -0
- package/src/infrastructure/utils/index.ts +6 -0
- package/src/infrastructure/utils/utils.util.ts +150 -0
- package/src/presentation/hooks/cloudflare.hooks.ts +314 -0
- package/src/presentation/hooks/index.ts +6 -0
- package/src/worker.example.ts +41 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare AI Gateway Service
|
|
3
|
+
* @description AI Gateway for routing, caching, and analytics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
AIGatewayConfig,
|
|
8
|
+
AIRequest,
|
|
9
|
+
AIResponse,
|
|
10
|
+
AIProvider,
|
|
11
|
+
AIAnalytics,
|
|
12
|
+
} from '../../domain/ai-gateway.entity';
|
|
13
|
+
|
|
14
|
+
export class AIGatewayService {
|
|
15
|
+
private config: AIGatewayConfig;
|
|
16
|
+
private kv?: KVNamespace;
|
|
17
|
+
private analytics: Map<string, number>;
|
|
18
|
+
|
|
19
|
+
constructor(config: AIGatewayConfig, KV?: KVNamespace) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.kv = KV;
|
|
22
|
+
this.analytics = new Map();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Route AI request to appropriate provider
|
|
27
|
+
*/
|
|
28
|
+
async route(request: AIRequest): Promise<AIResponse> {
|
|
29
|
+
const startTime = Date.now();
|
|
30
|
+
|
|
31
|
+
// Check cache first
|
|
32
|
+
if (this.config.cacheEnabled && request.cacheKey) {
|
|
33
|
+
const cached = await this.getFromCache(request.cacheKey);
|
|
34
|
+
if (cached) {
|
|
35
|
+
return {
|
|
36
|
+
...cached,
|
|
37
|
+
cached: true,
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Select provider (load balancing or fallback)
|
|
44
|
+
const provider = this.selectProvider(request.provider);
|
|
45
|
+
if (!provider) {
|
|
46
|
+
throw new Error(`Provider ${request.provider} not found`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Make request to provider
|
|
51
|
+
const response = await this.makeRequest(provider, request);
|
|
52
|
+
|
|
53
|
+
// Cache response
|
|
54
|
+
if (this.config.cacheEnabled && request.cacheKey) {
|
|
55
|
+
await this.saveToCache(request.cacheKey, response);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Track analytics
|
|
59
|
+
if (this.config.analytics) {
|
|
60
|
+
this.trackAnalytics(provider, response, Date.now() - startTime);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return response;
|
|
64
|
+
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Try fallback provider
|
|
67
|
+
if (provider.fallbackProvider) {
|
|
68
|
+
const fallback = this.config.providers.find(
|
|
69
|
+
(p) => p.id === provider.fallbackProvider
|
|
70
|
+
);
|
|
71
|
+
if (fallback) {
|
|
72
|
+
return this.makeRequest(fallback, request);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Select provider based on weight or specific provider
|
|
81
|
+
*/
|
|
82
|
+
private selectProvider(providerId?: string): AIProvider | null {
|
|
83
|
+
if (providerId) {
|
|
84
|
+
return this.config.providers.find((p) => p.id === providerId) || null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Load balancing based on weights
|
|
88
|
+
const totalWeight = this.config.providers.reduce(
|
|
89
|
+
(sum, p) => sum + (p.weight || 1),
|
|
90
|
+
0
|
|
91
|
+
);
|
|
92
|
+
let random = Math.random() * totalWeight;
|
|
93
|
+
|
|
94
|
+
for (const provider of this.config.providers) {
|
|
95
|
+
random -= provider.weight || 1;
|
|
96
|
+
if (random <= 0) {
|
|
97
|
+
return provider;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return this.config.providers[0] || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Make request to AI provider
|
|
106
|
+
*/
|
|
107
|
+
private async makeRequest(
|
|
108
|
+
provider: AIProvider,
|
|
109
|
+
request: AIRequest
|
|
110
|
+
): Promise<AIResponse> {
|
|
111
|
+
const url = `${provider.baseURL}/${request.model}`;
|
|
112
|
+
|
|
113
|
+
const response = await fetch(url, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
'Authorization': `Bearer ${provider.apiKey}`,
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
prompt: request.prompt,
|
|
121
|
+
...request.parameters,
|
|
122
|
+
stream: request.stream,
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
throw new Error(`Provider error: ${response.statusText}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const data = await response.json();
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
id: data.id || this.generateId(),
|
|
134
|
+
provider: provider.id,
|
|
135
|
+
model: request.model,
|
|
136
|
+
content: data.content || data.text || data.output,
|
|
137
|
+
usage: data.usage || {
|
|
138
|
+
promptTokens: 0,
|
|
139
|
+
completionTokens: 0,
|
|
140
|
+
totalTokens: 0,
|
|
141
|
+
},
|
|
142
|
+
cached: false,
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get response from cache
|
|
149
|
+
*/
|
|
150
|
+
private async getFromCache(key: string): Promise<AIResponse | null> {
|
|
151
|
+
if (this.kv) {
|
|
152
|
+
const data = await this.kv.get(`ai_cache:${key}`);
|
|
153
|
+
return data ? JSON.parse(data) : null;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Save response to cache
|
|
160
|
+
*/
|
|
161
|
+
private async saveToCache(key: string, response: AIResponse): Promise<void> {
|
|
162
|
+
if (this.kv && this.config.cacheTTL) {
|
|
163
|
+
await this.kv.put(
|
|
164
|
+
`ai_cache:${key}`,
|
|
165
|
+
JSON.stringify(response),
|
|
166
|
+
{ expirationTtl: this.config.cacheTTL }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Track analytics
|
|
173
|
+
*/
|
|
174
|
+
private trackAnalytics(
|
|
175
|
+
provider: AIProvider,
|
|
176
|
+
response: AIResponse,
|
|
177
|
+
duration: number
|
|
178
|
+
): void {
|
|
179
|
+
const key = `provider:${provider.id}`;
|
|
180
|
+
const currentCount = this.analytics.get(key) || 0;
|
|
181
|
+
this.analytics.set(key, currentCount + 1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get analytics
|
|
186
|
+
*/
|
|
187
|
+
async getAnalytics(): Promise<AIAnalytics> {
|
|
188
|
+
return {
|
|
189
|
+
totalRequests: Array.from(this.analytics.values()).reduce((a, b) => a + b, 0),
|
|
190
|
+
totalTokens: 0,
|
|
191
|
+
cacheHitRate: 0,
|
|
192
|
+
averageResponseTime: 0,
|
|
193
|
+
providerUsage: Object.fromEntries(this.analytics),
|
|
194
|
+
errorRate: 0,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private generateId(): string {
|
|
199
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Workers AI Service
|
|
205
|
+
* @description Direct integration with Cloudflare Workers AI
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
import type {
|
|
209
|
+
WorkersAIRequest,
|
|
210
|
+
WorkersAIResponse,
|
|
211
|
+
WorkersAIInputs,
|
|
212
|
+
ScriptGenerationRequest,
|
|
213
|
+
EmotionControl,
|
|
214
|
+
} from '../../domain/ai-gateway.entity';
|
|
215
|
+
|
|
216
|
+
export class WorkersAIService {
|
|
217
|
+
private env: {
|
|
218
|
+
AI?: AiTextGeneration;
|
|
219
|
+
bindings?: {
|
|
220
|
+
AI?: Ai;
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
constructor(env: { AI?: any; bindings?: any }) {
|
|
225
|
+
this.env = env;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Run text generation model
|
|
230
|
+
*/
|
|
231
|
+
async runTextGeneration(
|
|
232
|
+
model: string,
|
|
233
|
+
inputs: WorkersAIInputs['text_generation']
|
|
234
|
+
): Promise<WorkersAIResponse> {
|
|
235
|
+
try {
|
|
236
|
+
// @ts-ignore - Workers AI runtime binding
|
|
237
|
+
const ai = this.env.bindings?.AI || this.env.AI;
|
|
238
|
+
|
|
239
|
+
if (!ai) {
|
|
240
|
+
throw new Error('Workers AI binding not configured');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const response = await ai.run(model, inputs);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
success: true,
|
|
247
|
+
data: {
|
|
248
|
+
output: response.response || response.output || response.text,
|
|
249
|
+
},
|
|
250
|
+
model,
|
|
251
|
+
};
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
error: error instanceof Error ? error.message : String(error),
|
|
256
|
+
model,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Generate script with emotion control (from voice cloning app)
|
|
263
|
+
*/
|
|
264
|
+
async generateScript(request: ScriptGenerationRequest): Promise<WorkersAIResponse> {
|
|
265
|
+
const { topic, emotion, duration, style = 'casual', keywords = [] } = request;
|
|
266
|
+
|
|
267
|
+
// Build emotion-enhanced prompt
|
|
268
|
+
const emotionPrompt = this.buildEmotionPrompt(emotion);
|
|
269
|
+
const keywordText = keywords.length > 0 ? `\nKeywords: ${keywords.join(', ')}` : '';
|
|
270
|
+
const durationEstimate = Math.ceil(duration / 150); // ~150 words per minute
|
|
271
|
+
|
|
272
|
+
const prompt = `Write a ${style} ${durationEstimate}-minute script about: ${topic}
|
|
273
|
+
|
|
274
|
+
${emotionPrompt}
|
|
275
|
+
|
|
276
|
+
Requirements:
|
|
277
|
+
- Duration: approximately ${duration} seconds
|
|
278
|
+
- Tone: ${style}
|
|
279
|
+
- Target: Engaging, clear, and natural
|
|
280
|
+
- Format: Conversational speech${keywordText}
|
|
281
|
+
|
|
282
|
+
Generate the script:`;
|
|
283
|
+
|
|
284
|
+
return this.runTextGeneration('@cf/meta/llama-3.1-8b-instruct', {
|
|
285
|
+
prompt,
|
|
286
|
+
max_tokens: durationEstimate * 50,
|
|
287
|
+
temperature: 0.8 + (emotion.intensity * 0.2),
|
|
288
|
+
top_p: 0.9,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Build emotion prompt for script generation
|
|
294
|
+
*/
|
|
295
|
+
private buildEmotionPrompt(emotion: EmotionControl): string {
|
|
296
|
+
const intensityText =
|
|
297
|
+
emotion.intensity > 0.7 ? 'strongly ' :
|
|
298
|
+
emotion.intensity > 0.4 ? 'moderately ' :
|
|
299
|
+
'subtly ';
|
|
300
|
+
|
|
301
|
+
const emotionInstructions: Record<EmotionControl['emotion'], string> = {
|
|
302
|
+
neutral: 'Maintain a balanced, professional tone throughout',
|
|
303
|
+
happy: `Inject ${intensityText}positive, energetic language and enthusiastic expressions`,
|
|
304
|
+
sad: `Use ${intensityText}reflective, thoughtful language with a gentle tone`,
|
|
305
|
+
angry: `Incorporate ${intensityText}passionate, firm language with strong conviction`,
|
|
306
|
+
excited: `Use ${intensityText}dynamic, upbeat language with high energy expressions`,
|
|
307
|
+
calm: `Maintain ${intensityText}serene, peaceful language with a steady rhythm`,
|
|
308
|
+
surprised: `Include ${intensityText}wonder, discovery, and unexpected insights`,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
return emotionInstructions[emotion.emotion] || emotionInstructions.neutral;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Run image generation model
|
|
316
|
+
*/
|
|
317
|
+
async runImageGeneration(
|
|
318
|
+
model: string,
|
|
319
|
+
inputs: WorkersAIInputs['image_generation']
|
|
320
|
+
): Promise<WorkersAIResponse> {
|
|
321
|
+
try {
|
|
322
|
+
// @ts-ignore - Workers AI runtime binding
|
|
323
|
+
const ai = this.env.bindings?.AI || this.env.AI;
|
|
324
|
+
|
|
325
|
+
if (!ai) {
|
|
326
|
+
throw new Error('Workers AI binding not configured');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const response = await ai.run(model, inputs);
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
success: true,
|
|
333
|
+
data: {
|
|
334
|
+
image: response.image || response.output,
|
|
335
|
+
},
|
|
336
|
+
model,
|
|
337
|
+
};
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return {
|
|
340
|
+
success: false,
|
|
341
|
+
error: error instanceof Error ? error.message : String(error),
|
|
342
|
+
model,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Generate embedding
|
|
349
|
+
*/
|
|
350
|
+
async generateEmbedding(text: string): Promise<WorkersAIResponse> {
|
|
351
|
+
try {
|
|
352
|
+
// @ts-ignore - Workers AI runtime binding
|
|
353
|
+
const ai = this.env.bindings?.AI || this.env.AI;
|
|
354
|
+
|
|
355
|
+
if (!ai) {
|
|
356
|
+
throw new Error('Workers AI binding not configured');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const model = '@cf/openai/clip-vit-base-patch32';
|
|
360
|
+
const response = await ai.run(model, { text });
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
success: true,
|
|
364
|
+
data: {
|
|
365
|
+
embedding: response.embedding || response.output,
|
|
366
|
+
},
|
|
367
|
+
model,
|
|
368
|
+
};
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
error: error instanceof Error ? error.message : String(error),
|
|
373
|
+
model: '@cf/openai/clip-vit-base-patch32',
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Translate text
|
|
380
|
+
*/
|
|
381
|
+
async translate(
|
|
382
|
+
text: string,
|
|
383
|
+
sourceLang: string,
|
|
384
|
+
targetLang: string
|
|
385
|
+
): Promise<WorkersAIResponse> {
|
|
386
|
+
try {
|
|
387
|
+
// @ts-ignore - Workers AI runtime binding
|
|
388
|
+
const ai = this.env.bindings?.AI || this.env.AI;
|
|
389
|
+
|
|
390
|
+
if (!ai) {
|
|
391
|
+
throw new Error('Workers AI binding not configured');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Note: Translation model might vary
|
|
395
|
+
const response = await ai.run('@cf/meta/m2m100-1.2b', {
|
|
396
|
+
text,
|
|
397
|
+
source_lang: sourceLang,
|
|
398
|
+
target_lang: targetLang,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
success: true,
|
|
403
|
+
data: {
|
|
404
|
+
output: response.translated_text || response.output || response.text,
|
|
405
|
+
},
|
|
406
|
+
model: '@cf/meta/m2m100-1.2b',
|
|
407
|
+
};
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return {
|
|
410
|
+
success: false,
|
|
411
|
+
error: error instanceof Error ? error.message : String(error),
|
|
412
|
+
model: '@cf/meta/m2m100-1.2b',
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Service
|
|
3
|
+
* @description Cloudflare Web Analytics operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AnalyticsEvent, AnalyticsPageviewEvent, AnalyticsCustomEvent, AnalyticsData } from "../../../domain/entities/analytics.entity";
|
|
7
|
+
import type { IAnalyticsService } from "../../../domain/interfaces/services.interface";
|
|
8
|
+
|
|
9
|
+
export interface AnalyticsClientOptions {
|
|
10
|
+
readonly siteId: string;
|
|
11
|
+
readonly scriptUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class AnalyticsService implements IAnalyticsService {
|
|
15
|
+
private siteId: string | null = null;
|
|
16
|
+
private scriptUrl: string | null = null;
|
|
17
|
+
private eventQueue: AnalyticsEvent[] = [];
|
|
18
|
+
|
|
19
|
+
initialize(options: AnalyticsClientOptions): void {
|
|
20
|
+
this.siteId = options.siteId;
|
|
21
|
+
this.scriptUrl = options.scriptUrl ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private ensureInitialized(): void {
|
|
25
|
+
if (!this.siteId) {
|
|
26
|
+
throw new Error("AnalyticsService not initialized");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async trackEvent(event: AnalyticsEvent): Promise<void> {
|
|
31
|
+
this.ensureInitialized();
|
|
32
|
+
|
|
33
|
+
this.eventQueue.push(event);
|
|
34
|
+
|
|
35
|
+
// In a browser environment, send to Cloudflare Analytics
|
|
36
|
+
if (typeof window !== "undefined" && (window as any)._cfAnalytics) {
|
|
37
|
+
(window as any)._cfAnalytics.track(event);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async trackPageview(url: string, title: string, referrer?: string): Promise<void> {
|
|
42
|
+
const event: AnalyticsPageviewEvent = {
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
url,
|
|
45
|
+
eventType: "pageview",
|
|
46
|
+
title,
|
|
47
|
+
referrer,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
await this.trackEvent(event);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async trackCustom(eventName: string, data?: Record<string, unknown>): Promise<void> {
|
|
54
|
+
if (typeof window === "undefined") return;
|
|
55
|
+
|
|
56
|
+
const event: AnalyticsCustomEvent = {
|
|
57
|
+
timestamp: Date.now(),
|
|
58
|
+
url: window.location.href,
|
|
59
|
+
eventType: "custom",
|
|
60
|
+
eventName,
|
|
61
|
+
eventData: data,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
await this.trackEvent(event);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async trackOutboundLink(url: string, linkType?: string): Promise<void> {
|
|
68
|
+
if (typeof window === "undefined") return;
|
|
69
|
+
|
|
70
|
+
const event: AnalyticsCustomEvent = {
|
|
71
|
+
timestamp: Date.now(),
|
|
72
|
+
url: window.location.href,
|
|
73
|
+
eventType: "custom",
|
|
74
|
+
eventName: "outbound-link",
|
|
75
|
+
eventData: {
|
|
76
|
+
targetUrl: url,
|
|
77
|
+
linkType,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
await this.trackEvent(event);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async trackTiming(name: string, value: number, label?: string): Promise<void> {
|
|
85
|
+
if (typeof window === "undefined") return;
|
|
86
|
+
|
|
87
|
+
const event: AnalyticsEvent = {
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
url: window.location.href,
|
|
90
|
+
eventType: "timing",
|
|
91
|
+
eventData: {
|
|
92
|
+
name,
|
|
93
|
+
value,
|
|
94
|
+
label,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await this.trackEvent(event);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async getAnalytics(): Promise<AnalyticsData> {
|
|
102
|
+
this.ensureInitialized();
|
|
103
|
+
|
|
104
|
+
// In a real implementation, this would fetch from Cloudflare Analytics API
|
|
105
|
+
// For now, return queued events
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
siteId: this.siteId!,
|
|
109
|
+
events: this.eventQueue,
|
|
110
|
+
metrics: {
|
|
111
|
+
pageviews: this.eventQueue.filter((e) => e.eventType === "pageview").length,
|
|
112
|
+
uniqueVisitors: new Set(this.eventQueue.map((e) => e.url)).size,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get analytics script tag
|
|
119
|
+
*/
|
|
120
|
+
getScriptTag(): string {
|
|
121
|
+
this.ensureInitialized();
|
|
122
|
+
|
|
123
|
+
const scriptUrl = this.scriptUrl || `https://static.cloudflareinsights.com/beacon.min.js`;
|
|
124
|
+
|
|
125
|
+
return `
|
|
126
|
+
<script defer src='${scriptUrl}' data-cf-beacon='{"token": "${this.siteId}"}'></script>
|
|
127
|
+
`.trim();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Clear queued events
|
|
132
|
+
*/
|
|
133
|
+
clearEvents(): void {
|
|
134
|
+
this.eventQueue = [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get queued events
|
|
139
|
+
*/
|
|
140
|
+
getQueuedEvents(): readonly AnalyticsEvent[] {
|
|
141
|
+
return this.eventQueue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* E-commerce helpers
|
|
146
|
+
*/
|
|
147
|
+
async trackPurchase(transactionId: string, revenue: number, items: readonly { id: string; name: string; price: number; quantity: number }[]): Promise<void> {
|
|
148
|
+
await this.trackCustom("purchase", {
|
|
149
|
+
transactionId,
|
|
150
|
+
revenue,
|
|
151
|
+
items,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async trackAddToCart(itemId: string, price: number, quantity: number): Promise<void> {
|
|
156
|
+
await this.trackCustom("add-to-cart", {
|
|
157
|
+
itemId,
|
|
158
|
+
price,
|
|
159
|
+
quantity,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async trackRemoveFromCart(itemId: string, quantity: number): Promise<void> {
|
|
164
|
+
await this.trackCustom("remove-from-cart", {
|
|
165
|
+
itemId,
|
|
166
|
+
quantity,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Engagement helpers
|
|
172
|
+
*/
|
|
173
|
+
async trackScrollDepth(depth: number): Promise<void> {
|
|
174
|
+
await this.trackCustom("scroll-depth", { depth });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async trackTimeOnPage(seconds: number): Promise<void> {
|
|
178
|
+
await this.trackCustom("time-on-page", { seconds });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async trackEngagement(action: string, target?: string): Promise<void> {
|
|
182
|
+
await this.trackCustom("engagement", {
|
|
183
|
+
action,
|
|
184
|
+
target,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const analyticsService = new AnalyticsService();
|