@nextsparkjs/plugin-amplitude 0.1.0-beta.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/CODE_REVIEW_REPORT.md +462 -0
- package/README.md +619 -0
- package/__tests__/amplitude-core.test.ts +279 -0
- package/__tests__/hooks.test.ts +478 -0
- package/__tests__/validation.test.ts +393 -0
- package/components/AnalyticsDashboard.tsx +339 -0
- package/components/ConsentManager.tsx +265 -0
- package/components/ExperimentWrapper.tsx +440 -0
- package/components/PerformanceMonitor.tsx +578 -0
- package/hooks/useAmplitude.ts +132 -0
- package/hooks/useAmplitudeEvents.ts +100 -0
- package/hooks/useExperiment.ts +195 -0
- package/hooks/useSessionReplay.ts +238 -0
- package/jest.setup.ts +276 -0
- package/lib/amplitude-core.ts +178 -0
- package/lib/cache.ts +181 -0
- package/lib/performance.ts +319 -0
- package/lib/queue.ts +389 -0
- package/lib/security.ts +188 -0
- package/package.json +15 -0
- package/plugin.config.ts +58 -0
- package/providers/AmplitudeProvider.tsx +113 -0
- package/styles/amplitude.css +593 -0
- package/translations/en.json +45 -0
- package/translations/es.json +45 -0
- package/tsconfig.json +47 -0
- package/types/amplitude.types.ts +105 -0
- package/utils/debounce.ts +133 -0
package/tsconfig.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"baseUrl": ".",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"paths": {
|
|
8
|
+
"@/*": ["../../../*"],
|
|
9
|
+
"@/core/*": ["../../../core/*"],
|
|
10
|
+
"@/contents/*": ["../../../contents/*"],
|
|
11
|
+
"~/*": ["./*"]
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"watchOptions": {
|
|
15
|
+
"watchFile": "useFsEvents",
|
|
16
|
+
"watchDirectory": "useFsEvents",
|
|
17
|
+
"fallbackPolling": "dynamicPriority",
|
|
18
|
+
"synchronousWatchDirectory": false,
|
|
19
|
+
"excludeDirectories": [
|
|
20
|
+
"**/node_modules",
|
|
21
|
+
"**/.next",
|
|
22
|
+
"**/dist",
|
|
23
|
+
"**/build",
|
|
24
|
+
"**/.turbo",
|
|
25
|
+
"**/coverage",
|
|
26
|
+
"**/.git"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"include": [
|
|
30
|
+
"**/*.ts",
|
|
31
|
+
"**/*.tsx",
|
|
32
|
+
"plugin.config.ts"
|
|
33
|
+
],
|
|
34
|
+
"exclude": [
|
|
35
|
+
"node_modules",
|
|
36
|
+
".next",
|
|
37
|
+
"dist",
|
|
38
|
+
"build",
|
|
39
|
+
".turbo",
|
|
40
|
+
"coverage",
|
|
41
|
+
".git",
|
|
42
|
+
"**/*.test.ts",
|
|
43
|
+
"**/*.test.tsx",
|
|
44
|
+
"**/__tests__/**",
|
|
45
|
+
"**/tsconfig.tsbuildinfo"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export type AmplitudeAPIKey = string & { __brand: 'AmplitudeAPIKey' };
|
|
4
|
+
export type UserId = string & { __brand: 'UserId' };
|
|
5
|
+
export type EventType = string & { __brand: 'EventType' };
|
|
6
|
+
export type EventProperties = Record<string, any>;
|
|
7
|
+
export type UserProperties = Record<string, any>;
|
|
8
|
+
|
|
9
|
+
export const AmplitudeAPIKeySchema = z.string().regex(/^[a-zA-Z0-9]{32}$/).brand('AmplitudeAPIKey');
|
|
10
|
+
export const UserIdSchema = z.string().min(1).brand('UserId');
|
|
11
|
+
export const EventTypeSchema = z.string().min(1).brand('EventType');
|
|
12
|
+
export const EventPropertiesSchema = z.record(z.string(), z.any());
|
|
13
|
+
export const UserPropertiesSchema = z.record(z.string(), z.any());
|
|
14
|
+
|
|
15
|
+
export interface AmplitudePluginConfig {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
serverZone: 'US' | 'EU';
|
|
18
|
+
enableSessionReplay: boolean;
|
|
19
|
+
enableABTesting: boolean;
|
|
20
|
+
sampleRate: number;
|
|
21
|
+
enableConsentManagement: boolean;
|
|
22
|
+
batchSize: number;
|
|
23
|
+
flushInterval: number;
|
|
24
|
+
debugMode: boolean;
|
|
25
|
+
piiMaskingEnabled: boolean;
|
|
26
|
+
rateLimitEventsPerMinute: number;
|
|
27
|
+
errorRetryAttempts: number;
|
|
28
|
+
errorRetryDelayMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AmplitudeCore {
|
|
32
|
+
init(apiKey: AmplitudeAPIKey, config: AmplitudePluginConfig): Promise<void>;
|
|
33
|
+
track(eventType: EventType, properties?: EventProperties): Promise<void>;
|
|
34
|
+
identify(userId: UserId, properties?: UserProperties): Promise<void>;
|
|
35
|
+
setUserProperties(properties: UserProperties): Promise<void>;
|
|
36
|
+
reset(): void;
|
|
37
|
+
isInitialized(): boolean;
|
|
38
|
+
shutdown(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface UseAmplitudeResult {
|
|
42
|
+
track: (eventType: EventType, properties?: EventProperties) => Promise<void>;
|
|
43
|
+
identify: (userId: UserId, properties?: UserProperties) => Promise<void>;
|
|
44
|
+
setUserProperties: (properties: UserProperties) => Promise<void>;
|
|
45
|
+
reset: () => void;
|
|
46
|
+
isInitialized: boolean;
|
|
47
|
+
context: {
|
|
48
|
+
config: AmplitudePluginConfig | null;
|
|
49
|
+
consent: ConsentState;
|
|
50
|
+
error: Error | null;
|
|
51
|
+
};
|
|
52
|
+
lastError: Error | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UseExperimentResult {
|
|
56
|
+
getVariant: (experimentId: string, userId: string) => string | null;
|
|
57
|
+
trackExposure: (experimentId: string, variantId?: string) => Promise<void>;
|
|
58
|
+
trackConversion: (experimentId: string, metricId?: string, value?: number) => Promise<void>;
|
|
59
|
+
isInExperiment: (experimentId: string, userId: string) => boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface UseSessionReplayResult {
|
|
63
|
+
startRecording: () => Promise<boolean>;
|
|
64
|
+
stopRecording: () => Promise<void>;
|
|
65
|
+
pauseRecording: () => void;
|
|
66
|
+
resumeRecording: () => void;
|
|
67
|
+
isRecording: boolean;
|
|
68
|
+
canRecord: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AmplitudePluginContext {
|
|
72
|
+
amplitude: AmplitudeCore | null;
|
|
73
|
+
isInitialized: boolean;
|
|
74
|
+
config: AmplitudePluginConfig | null;
|
|
75
|
+
consent: ConsentState;
|
|
76
|
+
updateConsent: (consent: ConsentState) => void;
|
|
77
|
+
error: Error | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type ConsentCategory = 'analytics' | 'sessionReplay' | 'experiments' | 'performance';
|
|
81
|
+
export type ConsentState = Record<ConsentCategory, boolean>;
|
|
82
|
+
|
|
83
|
+
export interface ConsentManagerProps {
|
|
84
|
+
isOpen: boolean;
|
|
85
|
+
onClose: () => void;
|
|
86
|
+
onConsentChange: (consent: ConsentState) => void;
|
|
87
|
+
initialConsent?: Partial<ConsentState>;
|
|
88
|
+
showBadge?: boolean;
|
|
89
|
+
position?: 'bottom-left' | 'bottom-right' | 'center' | 'top';
|
|
90
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
91
|
+
compactMode?: boolean;
|
|
92
|
+
persistUserChoice?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isAmplitudeAPIKey(key: string): key is AmplitudeAPIKey {
|
|
96
|
+
return /^[a-zA-Z0-9]{32}$/.test(key);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function isValidUserId(id: any): id is UserId {
|
|
100
|
+
return typeof id === 'string' && id.trim().length > 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isAmplitudeEvent(event: any): event is { eventType: EventType; properties?: EventProperties } {
|
|
104
|
+
return Boolean(event && typeof event.eventType === 'string' && event.eventType.length > 0);
|
|
105
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export function debounce<T extends (...args: any[]) => any>(
|
|
2
|
+
func: T,
|
|
3
|
+
waitMs: number,
|
|
4
|
+
options: {
|
|
5
|
+
leading?: boolean;
|
|
6
|
+
trailing?: boolean;
|
|
7
|
+
maxWait?: number;
|
|
8
|
+
} = {}
|
|
9
|
+
): T & { cancel: () => void; flush: () => ReturnType<T> } {
|
|
10
|
+
let timeoutId: NodeJS.Timeout | null = null;
|
|
11
|
+
let maxTimeoutId: NodeJS.Timeout | null = null;
|
|
12
|
+
let lastCallTime: number | undefined;
|
|
13
|
+
let lastInvokeTime = 0;
|
|
14
|
+
let lastArgs: Parameters<T> | undefined;
|
|
15
|
+
let lastThis: any;
|
|
16
|
+
let result: ReturnType<T>;
|
|
17
|
+
|
|
18
|
+
const { leading = false, trailing = true, maxWait } = options;
|
|
19
|
+
|
|
20
|
+
function invokeFunc(time: number) {
|
|
21
|
+
const args = lastArgs!;
|
|
22
|
+
const thisArg = lastThis;
|
|
23
|
+
|
|
24
|
+
lastArgs = undefined;
|
|
25
|
+
lastThis = undefined;
|
|
26
|
+
lastInvokeTime = time;
|
|
27
|
+
result = func.apply(thisArg, args);
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function startTimer(pendingFunc: () => void, wait: number) {
|
|
32
|
+
return setTimeout(pendingFunc, wait);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cancelTimer(id: NodeJS.Timeout) {
|
|
36
|
+
clearTimeout(id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function leadingEdge(time: number) {
|
|
40
|
+
lastInvokeTime = time;
|
|
41
|
+
timeoutId = startTimer(timerExpired, waitMs);
|
|
42
|
+
return leading ? invokeFunc(time) : result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function remainingWait(time: number) {
|
|
46
|
+
const timeSinceLastCall = time - lastCallTime!;
|
|
47
|
+
const timeSinceLastInvoke = time - lastInvokeTime;
|
|
48
|
+
const timeWaiting = waitMs - timeSinceLastCall;
|
|
49
|
+
|
|
50
|
+
return maxWait !== undefined
|
|
51
|
+
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
|
|
52
|
+
: timeWaiting;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function shouldInvoke(time: number) {
|
|
56
|
+
const timeSinceLastCall = time - lastCallTime!;
|
|
57
|
+
const timeSinceLastInvoke = time - lastInvokeTime;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
lastCallTime === undefined ||
|
|
61
|
+
timeSinceLastCall >= waitMs ||
|
|
62
|
+
timeSinceLastCall < 0 ||
|
|
63
|
+
(maxWait !== undefined && timeSinceLastInvoke >= maxWait)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function timerExpired() {
|
|
68
|
+
const time = Date.now();
|
|
69
|
+
if (shouldInvoke(time)) {
|
|
70
|
+
return trailingEdge(time);
|
|
71
|
+
}
|
|
72
|
+
timeoutId = startTimer(timerExpired, remainingWait(time));
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function trailingEdge(time: number) {
|
|
77
|
+
timeoutId = null;
|
|
78
|
+
|
|
79
|
+
if (trailing && lastArgs) {
|
|
80
|
+
return invokeFunc(time);
|
|
81
|
+
}
|
|
82
|
+
lastArgs = undefined;
|
|
83
|
+
lastThis = undefined;
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function cancel() {
|
|
88
|
+
if (timeoutId !== null) {
|
|
89
|
+
cancelTimer(timeoutId);
|
|
90
|
+
}
|
|
91
|
+
if (maxTimeoutId !== null) {
|
|
92
|
+
cancelTimer(maxTimeoutId);
|
|
93
|
+
}
|
|
94
|
+
lastInvokeTime = 0;
|
|
95
|
+
lastArgs = undefined;
|
|
96
|
+
lastCallTime = undefined;
|
|
97
|
+
lastThis = undefined;
|
|
98
|
+
timeoutId = null;
|
|
99
|
+
maxTimeoutId = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function flush() {
|
|
103
|
+
return timeoutId === null ? result : trailingEdge(Date.now());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function debounced(this: any, ...args: Parameters<T>): ReturnType<T> {
|
|
107
|
+
const time = Date.now();
|
|
108
|
+
const isInvoking = shouldInvoke(time);
|
|
109
|
+
|
|
110
|
+
lastArgs = args;
|
|
111
|
+
lastThis = this;
|
|
112
|
+
lastCallTime = time;
|
|
113
|
+
|
|
114
|
+
if (isInvoking) {
|
|
115
|
+
if (timeoutId === null) {
|
|
116
|
+
return leadingEdge(lastCallTime);
|
|
117
|
+
}
|
|
118
|
+
if (maxWait !== undefined) {
|
|
119
|
+
maxTimeoutId = startTimer(timerExpired, maxWait);
|
|
120
|
+
return invokeFunc(lastCallTime);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (timeoutId === null) {
|
|
124
|
+
timeoutId = startTimer(timerExpired, waitMs);
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
debounced.cancel = cancel;
|
|
130
|
+
debounced.flush = flush;
|
|
131
|
+
|
|
132
|
+
return debounced as T & { cancel: () => void; flush: () => ReturnType<T> };
|
|
133
|
+
}
|