@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.
@@ -0,0 +1,188 @@
1
+ import { EventProperties, UserProperties } from '../types/amplitude.types';
2
+
3
+ export type PIIPattern = {
4
+ regex: RegExp;
5
+ mask: string;
6
+ };
7
+
8
+ export class DataSanitizer {
9
+ public static sanitizeEventProperties(properties: EventProperties | undefined, piiPatterns: PIIPattern[]): EventProperties | undefined {
10
+ if (!properties) return properties;
11
+
12
+ const sanitized = { ...properties };
13
+
14
+ for (const [key, value] of Object.entries(sanitized)) {
15
+ if (typeof value === 'string') {
16
+ sanitized[key] = this.sanitizeString(value, piiPatterns);
17
+ } else if (typeof value === 'object' && value !== null) {
18
+ sanitized[key] = this.sanitizeEventProperties(value, piiPatterns);
19
+ }
20
+ }
21
+
22
+ return sanitized;
23
+ }
24
+
25
+ public static sanitizeUserProperties(properties: UserProperties | undefined, piiPatterns: PIIPattern[]): UserProperties | undefined {
26
+ if (!properties) return properties;
27
+
28
+ const sanitized = { ...properties };
29
+
30
+ for (const [key, value] of Object.entries(sanitized)) {
31
+ if (typeof value === 'string') {
32
+ sanitized[key] = this.sanitizeString(value, piiPatterns);
33
+ } else if (typeof value === 'object' && value !== null) {
34
+ sanitized[key] = this.sanitizeUserProperties(value, piiPatterns);
35
+ }
36
+ }
37
+
38
+ return sanitized;
39
+ }
40
+
41
+ private static sanitizeString(text: string, piiPatterns: PIIPattern[]): string {
42
+ let sanitized = text;
43
+
44
+ for (const pattern of piiPatterns) {
45
+ sanitized = sanitized.replace(pattern.regex, pattern.mask);
46
+ }
47
+
48
+ return sanitized;
49
+ }
50
+ }
51
+
52
+ interface RateLimitEntry {
53
+ count: number;
54
+ firstRequest: number;
55
+ lastRequest: number;
56
+ }
57
+
58
+ export class SlidingWindowRateLimiter {
59
+ private requests = new Map<string, RateLimitEntry>();
60
+ private maxRequests: number;
61
+ private windowMs: number;
62
+
63
+ constructor(maxRequests: number, windowMs: number) {
64
+ this.maxRequests = maxRequests;
65
+ this.windowMs = windowMs;
66
+ }
67
+
68
+ public checkRateLimit(identifier: string): boolean {
69
+ const now = Date.now();
70
+ const entry = this.requests.get(identifier);
71
+
72
+ if (!entry) {
73
+ this.requests.set(identifier, {
74
+ count: 1,
75
+ firstRequest: now,
76
+ lastRequest: now,
77
+ });
78
+ return true;
79
+ }
80
+
81
+ // Clean old entries
82
+ if (now - entry.firstRequest > this.windowMs) {
83
+ this.requests.set(identifier, {
84
+ count: 1,
85
+ firstRequest: now,
86
+ lastRequest: now,
87
+ });
88
+ return true;
89
+ }
90
+
91
+ // Check if under limit
92
+ if (entry.count < this.maxRequests) {
93
+ entry.count++;
94
+ entry.lastRequest = now;
95
+ return true;
96
+ }
97
+
98
+ return false;
99
+ }
100
+
101
+ public getRemainingRequests(identifier: string): number {
102
+ const entry = this.requests.get(identifier);
103
+ if (!entry) return this.maxRequests;
104
+
105
+ const now = Date.now();
106
+ if (now - entry.firstRequest > this.windowMs) {
107
+ return this.maxRequests;
108
+ }
109
+
110
+ return Math.max(0, this.maxRequests - entry.count);
111
+ }
112
+ }
113
+
114
+ export type AuditLogSeverity = 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL';
115
+
116
+ export interface AuditLogEntry {
117
+ id: string;
118
+ timestamp: number;
119
+ event: string;
120
+ data: any;
121
+ severity: AuditLogSeverity;
122
+ source: string;
123
+ }
124
+
125
+ export class SecurityAuditLogger {
126
+ private logs: AuditLogEntry[] = [];
127
+ private maxLogs: number;
128
+ private retentionDays: number;
129
+
130
+ constructor(retentionDays: number = 30, maxLogs: number = 10000) {
131
+ this.retentionDays = retentionDays;
132
+ this.maxLogs = maxLogs;
133
+ }
134
+
135
+ public log(event: string, data: any, severity: AuditLogSeverity = 'INFO', source: string = 'amplitude-plugin'): void {
136
+ const entry: AuditLogEntry = {
137
+ id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
138
+ timestamp: Date.now(),
139
+ event,
140
+ data,
141
+ severity,
142
+ source,
143
+ };
144
+
145
+ this.logs.push(entry);
146
+ this.cleanup();
147
+
148
+ // Log to console for debugging
149
+ const logLevel = severity === 'CRITICAL' || severity === 'ERROR' ? 'error' :
150
+ severity === 'WARN' ? 'warn' : 'log';
151
+ console[logLevel](`[Audit] ${event}:`, data);
152
+ }
153
+
154
+ public getLogs(startTime?: number, endTime?: number, severity?: AuditLogSeverity): AuditLogEntry[] {
155
+ let filteredLogs = this.logs;
156
+
157
+ if (startTime) {
158
+ filteredLogs = filteredLogs.filter(log => log.timestamp >= startTime);
159
+ }
160
+
161
+ if (endTime) {
162
+ filteredLogs = filteredLogs.filter(log => log.timestamp <= endTime);
163
+ }
164
+
165
+ if (severity) {
166
+ filteredLogs = filteredLogs.filter(log => log.severity === severity);
167
+ }
168
+
169
+ return filteredLogs.sort((a, b) => b.timestamp - a.timestamp);
170
+ }
171
+
172
+ private cleanup(): void {
173
+ const now = Date.now();
174
+ const cutoffTime = now - (this.retentionDays * 24 * 60 * 60 * 1000);
175
+
176
+ // Remove old logs
177
+ this.logs = this.logs.filter(log => log.timestamp > cutoffTime);
178
+
179
+ // Remove excess logs
180
+ if (this.logs.length > this.maxLogs) {
181
+ this.logs = this.logs.slice(-this.maxLogs);
182
+ }
183
+ }
184
+ }
185
+
186
+ export const dataSanitizer = DataSanitizer;
187
+ export const rateLimiter = new SlidingWindowRateLimiter(1000, 60000); // 1000 requests per minute
188
+ export const auditLogger = new SecurityAuditLogger(30);
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@nextsparkjs/plugin-amplitude",
3
+ "version": "0.1.0-beta.1",
4
+ "private": false,
5
+ "main": "./plugin.config.ts",
6
+ "requiredPlugins": [],
7
+ "dependencies": {},
8
+ "peerDependencies": {
9
+ "@nextsparkjs/core": "workspace:*",
10
+ "next": "^15.0.0",
11
+ "react": "^19.0.0",
12
+ "react-dom": "^19.0.0",
13
+ "zod": "^4.0.0"
14
+ }
15
+ }
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ import type { PluginConfig } from '@nextsparkjs/core/types/plugin';
3
+
4
+ const AmplitudePluginConfigSchema = z.object({
5
+ apiKey: z.string().regex(/^[a-zA-Z0-9]{32}$/, "Invalid Amplitude API Key format").describe("Amplitude API Key"),
6
+ serverZone: z.enum(['US', 'EU']).default('US').describe("Amplitude server zone"),
7
+ enableSessionReplay: z.boolean().default(false).describe("Enable session replay recording"),
8
+ enableABTesting: z.boolean().default(false).describe("Enable A/B testing experiments"),
9
+ sampleRate: z.number().min(0).max(1).default(1).describe("Event sampling rate (0-1)"),
10
+ enableConsentManagement: z.boolean().default(true).describe("Enable GDPR/CCPA consent management"),
11
+ batchSize: z.number().min(1).max(100).default(30).describe("Number of events to batch before sending"),
12
+ flushInterval: z.number().min(1000).max(60000).default(10000).describe("Interval in ms to flush event queue"),
13
+ debugMode: z.boolean().default(false).describe("Enable debug logging for Amplitude plugin"),
14
+ piiMaskingEnabled: z.boolean().default(true).describe("Enable automatic PII masking"),
15
+ rateLimitEventsPerMinute: z.number().min(10).max(5000).default(1000).describe("Max events per minute per user/IP"),
16
+ errorRetryAttempts: z.number().min(0).max(5).default(3).describe("Number of retry attempts for failed events"),
17
+ errorRetryDelayMs: z.number().min(100).max(10000).default(1000).describe("Delay in ms between retry attempts"),
18
+ });
19
+
20
+ export const amplitudePlugin: PluginConfig = {
21
+ name: 'amplitude',
22
+ version: '1.0.0',
23
+ displayName: 'Amplitude Analytics',
24
+ description: 'Advanced user analytics and behavioral tracking plugin for SaaS applications.',
25
+ enabled: true,
26
+ dependencies: [],
27
+
28
+ // Plugin components that can be used throughout the app
29
+ components: {
30
+ ConsentManager: undefined, // Would contain actual component
31
+ AnalyticsDashboard: undefined,
32
+ ExperimentWrapper: undefined,
33
+ PerformanceMonitor: undefined
34
+ },
35
+
36
+ // Plugin services/hooks that can be used by other parts of the app
37
+ services: {
38
+ // Would contain amplitude-specific services
39
+ },
40
+
41
+ // Plugin lifecycle hooks
42
+ hooks: {
43
+ async onLoad() {
44
+ console.log('[Amplitude Plugin] Loading...');
45
+ },
46
+ async onActivate() {
47
+ console.log('[Amplitude Plugin] Activated');
48
+ },
49
+ async onDeactivate() {
50
+ console.log('[Amplitude Plugin] Deactivated');
51
+ },
52
+ async onUnload() {
53
+ console.log('[Amplitude Plugin] Unloaded');
54
+ }
55
+ }
56
+ };
57
+
58
+ export default amplitudePlugin;
@@ -0,0 +1,113 @@
1
+ import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
2
+ import { AmplitudeCore } from '../lib/amplitude-core';
3
+ import { amplitudePlugin } from '../plugin.config';
4
+ import { AmplitudePluginConfig, AmplitudePluginContext, ConsentState, isAmplitudeAPIKey } from '../types/amplitude.types';
5
+
6
+ const AmplitudeContext = createContext<AmplitudePluginContext | undefined>(undefined);
7
+
8
+ export const AmplitudeProvider: React.FC<{ children: React.ReactNode; overrideConfig?: Partial<AmplitudePluginConfig> }> = ({
9
+ children,
10
+ overrideConfig
11
+ }) => {
12
+ const [isInitialized, setIsInitialized] = useState(false);
13
+ const [error, setError] = useState<Error | null>(null);
14
+ const [consent, setConsent] = useState<ConsentState>({
15
+ analytics: false,
16
+ sessionReplay: false,
17
+ experiments: false,
18
+ performance: false
19
+ });
20
+
21
+ // TODO: Fix plugin config schema integration
22
+ const pluginConfig = overrideConfig as AmplitudePluginConfig;
23
+ const configLoading = false;
24
+ const configError = null;
25
+
26
+ const currentConfig = useRef<AmplitudePluginConfig | null>(null);
27
+ const retryCount = useRef(0);
28
+ const MAX_RETRIES = pluginConfig?.errorRetryAttempts || 3;
29
+ const RETRY_DELAY_MS = pluginConfig?.errorRetryDelayMs || 1000;
30
+
31
+ const initializeAmplitude = useCallback(async (config: AmplitudePluginConfig) => {
32
+ if (!config.apiKey || !isAmplitudeAPIKey(config.apiKey)) {
33
+ throw new Error('Invalid Amplitude API key');
34
+ }
35
+
36
+ try {
37
+ await AmplitudeCore.init(config.apiKey as any, config);
38
+ setIsInitialized(true);
39
+ setError(null);
40
+ retryCount.current = 0;
41
+ console.log('[Amplitude] Successfully initialized');
42
+ } catch (err) {
43
+ const error = err instanceof Error ? err : new Error('Failed to initialize Amplitude');
44
+
45
+ if (retryCount.current < MAX_RETRIES) {
46
+ retryCount.current++;
47
+ console.warn(`[Amplitude] Initialization failed, retrying in ${RETRY_DELAY_MS}ms (attempt ${retryCount.current}/${MAX_RETRIES})`);
48
+
49
+ setTimeout(() => {
50
+ initializeAmplitude(config);
51
+ }, RETRY_DELAY_MS);
52
+ } else {
53
+ setError(error);
54
+ setIsInitialized(false);
55
+ console.error('[Amplitude] Initialization failed after max retries:', error);
56
+ }
57
+ }
58
+ }, [MAX_RETRIES, RETRY_DELAY_MS]);
59
+
60
+ const updateConsent = useCallback((newConsent: ConsentState) => {
61
+ setConsent(newConsent);
62
+ localStorage.setItem('amplitude_consent', JSON.stringify(newConsent));
63
+ }, []);
64
+
65
+ useEffect(() => {
66
+ const savedConsent = localStorage.getItem('amplitude_consent');
67
+ if (savedConsent) {
68
+ try {
69
+ setConsent(JSON.parse(savedConsent));
70
+ } catch (error) {
71
+ console.warn('[Amplitude] Failed to parse saved consent:', error);
72
+ }
73
+ }
74
+ }, []);
75
+
76
+ useEffect(() => {
77
+ if (pluginConfig && !configLoading && !configError && !isInitialized && !error) {
78
+ currentConfig.current = { ...pluginConfig, ...overrideConfig };
79
+ initializeAmplitude(currentConfig.current);
80
+ }
81
+ }, [pluginConfig, configLoading, configError, isInitialized, error, initializeAmplitude, overrideConfig]);
82
+
83
+ const contextValue: AmplitudePluginContext = {
84
+ amplitude: AmplitudeCore,
85
+ isInitialized,
86
+ config: currentConfig.current,
87
+ consent,
88
+ updateConsent,
89
+ error,
90
+ };
91
+
92
+ if (configLoading) {
93
+ return <div>Loading Amplitude configuration...</div>;
94
+ }
95
+
96
+ if (error && !isInitialized) {
97
+ return <div>Amplitude initialization failed: {error.message}</div>;
98
+ }
99
+
100
+ return (
101
+ <AmplitudeContext.Provider value={contextValue}>
102
+ {children}
103
+ </AmplitudeContext.Provider>
104
+ );
105
+ };
106
+
107
+ export const useAmplitudeContext = () => {
108
+ const context = useContext(AmplitudeContext);
109
+ if (context === undefined) {
110
+ throw new Error('useAmplitudeContext must be used within an AmplitudeProvider');
111
+ }
112
+ return context;
113
+ };