eventlog-rn 0.3.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/README.md +64 -0
- package/dist/index.d.mts +206 -0
- package/dist/index.d.ts +206 -0
- package/dist/index.js +1170 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1142 -0
- package/dist/index.mjs.map +1 -0
- package/dist/viewer/index.d.mts +19 -0
- package/dist/viewer/index.d.ts +19 -0
- package/dist/viewer/index.js +1100 -0
- package/dist/viewer/index.js.map +1 -0
- package/dist/viewer/index.mjs +1075 -0
- package/dist/viewer/index.mjs.map +1 -0
- package/package.json +60 -0
- package/src/components/EventLogErrorBoundary.tsx +109 -0
- package/src/core/buffer.ts +66 -0
- package/src/core/errors.ts +79 -0
- package/src/core/eventlog.ts +463 -0
- package/src/core/events.ts +256 -0
- package/src/core/network.ts +209 -0
- package/src/core/query.ts +50 -0
- package/src/core/utils.ts +54 -0
- package/src/index.ts +39 -0
- package/src/instance.ts +7 -0
- package/src/storage/mmkv.ts +57 -0
- package/src/types.ts +159 -0
- package/src/viewer/EventItem.tsx +63 -0
- package/src/viewer/EventList.tsx +41 -0
- package/src/viewer/EventLogViewer.tsx +48 -0
- package/src/viewer/FilterBar.tsx +25 -0
- package/src/viewer/FilterChip.tsx +32 -0
- package/src/viewer/Header.tsx +24 -0
- package/src/viewer/SearchBar.tsx +20 -0
- package/src/viewer/hooks.ts +78 -0
- package/src/viewer/index.ts +6 -0
- package/src/viewer/styles.ts +151 -0
- package/src/viewer/types.ts +59 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network interception logic (Fetch & XHR)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EventLog, FeatureConfig, NetworkEventPayload } from '../types';
|
|
6
|
+
|
|
7
|
+
type NetworkConfig = NonNullable<FeatureConfig['network']>;
|
|
8
|
+
|
|
9
|
+
// Keep original globals
|
|
10
|
+
const originalFetch = global.fetch;
|
|
11
|
+
const originalXMLHttpRequest = global.XMLHttpRequest;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Intercept Fetch API
|
|
15
|
+
*/
|
|
16
|
+
const interceptFetch = (eventLog: EventLog, config: NetworkConfig) => {
|
|
17
|
+
if (!config.interceptFetch) return;
|
|
18
|
+
|
|
19
|
+
global.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
20
|
+
const startTime = Date.now();
|
|
21
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
22
|
+
const method = (init?.method ?? 'GET').toUpperCase();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response = await originalFetch(input, init);
|
|
26
|
+
const endTime = Date.now();
|
|
27
|
+
const duration = endTime - startTime;
|
|
28
|
+
|
|
29
|
+
// Clone response to read body without consuming original stream
|
|
30
|
+
const clone = response.clone();
|
|
31
|
+
|
|
32
|
+
let responseBody: unknown;
|
|
33
|
+
if (config.logResponseBody) {
|
|
34
|
+
try {
|
|
35
|
+
// Text first to check size/parse JSON
|
|
36
|
+
const text = await clone.text();
|
|
37
|
+
if (!config.maxBodySize || text.length <= config.maxBodySize) {
|
|
38
|
+
try {
|
|
39
|
+
responseBody = JSON.parse(text);
|
|
40
|
+
} catch {
|
|
41
|
+
responseBody = text;
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
responseBody = `[Body too large: ${text.length} bytes]`;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
responseBody = '[Failed to read response body]';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const payload: NetworkEventPayload = {
|
|
52
|
+
url,
|
|
53
|
+
method,
|
|
54
|
+
status: response.status,
|
|
55
|
+
duration,
|
|
56
|
+
responseHeaders: config.redactHeaders ? redactHeaders(response.headers, config.redactHeaders) : headersToObject(response.headers),
|
|
57
|
+
responseBody,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
eventLog.network(payload);
|
|
61
|
+
|
|
62
|
+
return response;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const endTime = Date.now();
|
|
65
|
+
eventLog.error(error, {
|
|
66
|
+
url,
|
|
67
|
+
method,
|
|
68
|
+
duration: endTime - startTime,
|
|
69
|
+
category: 'network', // Context hack
|
|
70
|
+
});
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Intercept XMLHttpRequest
|
|
78
|
+
*/
|
|
79
|
+
const interceptXHR = (eventLog: EventLog, config: NetworkConfig) => {
|
|
80
|
+
// If explicitly disabled, return. Undefined means enabled by default.
|
|
81
|
+
if (config.interceptAxios === false) return;
|
|
82
|
+
|
|
83
|
+
const XHR = originalXMLHttpRequest;
|
|
84
|
+
|
|
85
|
+
// @ts-ignore - Monkey patching
|
|
86
|
+
global.XMLHttpRequest = function() {
|
|
87
|
+
const xhr = new XHR();
|
|
88
|
+
const startTime = Date.now();
|
|
89
|
+
let method = 'GET';
|
|
90
|
+
let url = '';
|
|
91
|
+
|
|
92
|
+
const originalOpen = xhr.open;
|
|
93
|
+
const originalSend = xhr.send;
|
|
94
|
+
|
|
95
|
+
xhr.open = function(m: string, u: string, ...args: any[]) {
|
|
96
|
+
method = m.toUpperCase();
|
|
97
|
+
url = u;
|
|
98
|
+
return originalOpen.apply(this, [m, u, ...args] as any);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
xhr.send = function(body?: any) {
|
|
102
|
+
xhr.addEventListener('loadend', () => {
|
|
103
|
+
const duration = Date.now() - startTime;
|
|
104
|
+
|
|
105
|
+
// Extract headers (messy in XHR)
|
|
106
|
+
const responseHeadersString = xhr.getAllResponseHeaders();
|
|
107
|
+
const responseHeaders = parseXHRHeaders(responseHeadersString);
|
|
108
|
+
|
|
109
|
+
let responseBody: unknown;
|
|
110
|
+
if (config.logResponseBody) {
|
|
111
|
+
try {
|
|
112
|
+
if (xhr.responseType === '' || xhr.responseType === 'text') {
|
|
113
|
+
responseBody = xhr.responseText;
|
|
114
|
+
if (responseBody && typeof responseBody === 'string' && (!config.maxBodySize || responseBody.length <= config.maxBodySize)) {
|
|
115
|
+
try { responseBody = JSON.parse(responseBody); } catch {}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const payload: NetworkEventPayload = {
|
|
122
|
+
url,
|
|
123
|
+
method,
|
|
124
|
+
status: xhr.status,
|
|
125
|
+
duration,
|
|
126
|
+
responseHeaders: config.redactHeaders ? redactHeaders(responseHeaders, config.redactHeaders) : responseHeaders as any,
|
|
127
|
+
responseBody,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// @ts-ignore - We will add this method
|
|
131
|
+
if (typeof eventLog.network === 'function') {
|
|
132
|
+
// @ts-ignore
|
|
133
|
+
eventLog.network(payload);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return originalSend.apply(this, [body]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return xhr;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Restore prototype chain
|
|
143
|
+
global.XMLHttpRequest.prototype = XHR.prototype;
|
|
144
|
+
Object.assign(global.XMLHttpRequest, XHR);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Helpers
|
|
149
|
+
*/
|
|
150
|
+
const headersToObject = (headers: Headers): Record<string, string> => {
|
|
151
|
+
const obj: Record<string, string> = {};
|
|
152
|
+
headers.forEach((value, key) => {
|
|
153
|
+
obj[key] = value;
|
|
154
|
+
});
|
|
155
|
+
return obj;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const redactHeaders = (headers: Headers | Record<string, string>, keysToRedact: ReadonlyArray<string>): Record<string, string> => {
|
|
159
|
+
const obj = headers instanceof Headers ? headersToObject(headers) : headers;
|
|
160
|
+
const redacted: Record<string, string> = { ...obj };
|
|
161
|
+
keysToRedact.forEach(key => {
|
|
162
|
+
const lowerKey = key.toLowerCase();
|
|
163
|
+
// Simple case-insensitive match simulation
|
|
164
|
+
Object.keys(redacted).forEach(h => {
|
|
165
|
+
if (h.toLowerCase() === lowerKey) {
|
|
166
|
+
redacted[h] = '***REDACTED***';
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
return redacted;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const parseXHRHeaders = (headers: string): Record<string, string> => {
|
|
174
|
+
if (!headers) return {};
|
|
175
|
+
const result: Record<string, string> = {};
|
|
176
|
+
const pairs = headers.trim().split(/[\r\n]+/);
|
|
177
|
+
pairs.forEach(line => {
|
|
178
|
+
const parts = line.split(': ');
|
|
179
|
+
if (parts.length >= 2) {
|
|
180
|
+
const key = parts.shift()!;
|
|
181
|
+
const value = parts.join(': ');
|
|
182
|
+
result[key] = value;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Main enable function
|
|
190
|
+
*/
|
|
191
|
+
export const enableNetworkInterception = (eventLog: EventLog, config: FeatureConfig['network']) => {
|
|
192
|
+
if (!config || !config.enabled) return;
|
|
193
|
+
|
|
194
|
+
// Cleanup first just in case
|
|
195
|
+
restoreNetworkInterception();
|
|
196
|
+
|
|
197
|
+
if (config.interceptFetch !== false) {
|
|
198
|
+
interceptFetch(eventLog, config);
|
|
199
|
+
}
|
|
200
|
+
// Default to true for XHR if not specified, or checks interceptAxios
|
|
201
|
+
if (config.interceptAxios !== false) {
|
|
202
|
+
interceptXHR(eventLog, config);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const restoreNetworkInterception = () => {
|
|
207
|
+
global.fetch = originalFetch;
|
|
208
|
+
global.XMLHttpRequest = originalXMLHttpRequest;
|
|
209
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query events by filters
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Event, EventQuery } from '../types';
|
|
6
|
+
|
|
7
|
+
export const queryEvents = (
|
|
8
|
+
events: ReadonlyArray<Event>,
|
|
9
|
+
query: EventQuery
|
|
10
|
+
): ReadonlyArray<Event> => {
|
|
11
|
+
let filtered = events;
|
|
12
|
+
|
|
13
|
+
// Filter by category
|
|
14
|
+
if (query.category && query.category.length > 0) {
|
|
15
|
+
filtered = filtered.filter((e) => {
|
|
16
|
+
const categories = query.category;
|
|
17
|
+
return categories ? categories.includes(e.category) : true;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Filter by time range
|
|
22
|
+
if (query.timeRange) {
|
|
23
|
+
filtered = filtered.filter(
|
|
24
|
+
(e) =>
|
|
25
|
+
e.timestamp >= query.timeRange!.start &&
|
|
26
|
+
e.timestamp <= query.timeRange!.end
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Filter by session ID
|
|
31
|
+
if (query.sessionId) {
|
|
32
|
+
filtered = filtered.filter((e) => e.sessionId === query.sessionId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Search in payload
|
|
36
|
+
if (query.search) {
|
|
37
|
+
const searchLower = query.search.toLowerCase();
|
|
38
|
+
filtered = filtered.filter((e) => {
|
|
39
|
+
const payloadStr = JSON.stringify(e.payload).toLowerCase();
|
|
40
|
+
return payloadStr.includes(searchLower);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Limit results
|
|
45
|
+
if (query.limit && query.limit > 0) {
|
|
46
|
+
filtered = filtered.slice(-query.limit);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return filtered;
|
|
50
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a UUID v4
|
|
7
|
+
*/
|
|
8
|
+
export const generateUUID = (): string => {
|
|
9
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
10
|
+
const r = (Math.random() * 16) | 0;
|
|
11
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
12
|
+
return v.toString(16);
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get current timestamp in milliseconds
|
|
18
|
+
*/
|
|
19
|
+
export const getTimestamp = (): number => Date.now();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get ISO 8601 timestamp with timezone
|
|
23
|
+
*/
|
|
24
|
+
export const getTimestampISO = (): string => new Date().toISOString();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get IANA timezone identifier
|
|
28
|
+
*/
|
|
29
|
+
export const getTimezone = (): string => {
|
|
30
|
+
try {
|
|
31
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
32
|
+
} catch {
|
|
33
|
+
return 'UTC';
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Debounce function (returns a new function, pure in concept)
|
|
39
|
+
*/
|
|
40
|
+
export const debounce = <T extends ReadonlyArray<unknown>>(
|
|
41
|
+
func: (...args: T) => void,
|
|
42
|
+
wait: number
|
|
43
|
+
): ((...args: T) => void) => {
|
|
44
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
45
|
+
|
|
46
|
+
return (...args: T): void => {
|
|
47
|
+
if (timeout !== null) {
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
}
|
|
50
|
+
timeout = setTimeout(() => {
|
|
51
|
+
func(...args);
|
|
52
|
+
}, wait);
|
|
53
|
+
};
|
|
54
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eventlog-rn
|
|
3
|
+
* Functional, type-safe event logging SDK for React Native
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { createEventLog } from './core/eventlog';
|
|
7
|
+
export { EventLogViewer } from './viewer/EventLogViewer';
|
|
8
|
+
export type { EventLogViewerProps } from './viewer/types';
|
|
9
|
+
export type {
|
|
10
|
+
EventLog,
|
|
11
|
+
EventLogConfig,
|
|
12
|
+
Event,
|
|
13
|
+
EventCategory,
|
|
14
|
+
EventContext,
|
|
15
|
+
Session,
|
|
16
|
+
ExportOptions,
|
|
17
|
+
LogLevel,
|
|
18
|
+
Result,
|
|
19
|
+
StorageAdapter,
|
|
20
|
+
NetworkEventPayload,
|
|
21
|
+
ErrorEventPayload,
|
|
22
|
+
EventQuery,
|
|
23
|
+
} from './types';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Default singleton instance
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import { eventLog } from 'eventlog-rn';
|
|
31
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
32
|
+
*
|
|
33
|
+
* await eventLog.init({ storage: AsyncStorage });
|
|
34
|
+
* eventLog.screen('Home');
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
import { eventLog } from './instance';
|
|
38
|
+
export { eventLog };
|
|
39
|
+
export { EventLogErrorBoundary } from './components/EventLogErrorBoundary';
|
package/src/instance.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal MMKV storage adapter
|
|
3
|
+
* Supports both MMKV v3 (RN 0.74+) and v4 (RN 0.75+)
|
|
4
|
+
* MMKV is synchronous, but we wrap it in Promises to match StorageAdapter interface
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { StorageAdapter } from '../types';
|
|
8
|
+
|
|
9
|
+
// Detect and initialize MMKV with version compatibility
|
|
10
|
+
import * as MMKVModule from 'react-native-mmkv';
|
|
11
|
+
|
|
12
|
+
let mmkv: any;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
// Handle ESM vs CJS
|
|
16
|
+
const m = (MMKVModule as any)?.default ? (MMKVModule as any).default : MMKVModule;
|
|
17
|
+
|
|
18
|
+
if (typeof (m as any).createMMKV === 'function') {
|
|
19
|
+
// v4 API
|
|
20
|
+
mmkv = (m as any).createMMKV({ id: 'eventlog-rn' });
|
|
21
|
+
|
|
22
|
+
} else if (typeof (m as any).MMKV === 'function') {
|
|
23
|
+
// v3 API
|
|
24
|
+
mmkv = new (m as any).MMKV({ id: 'eventlog-rn' });
|
|
25
|
+
|
|
26
|
+
} else if (typeof m === 'function') {
|
|
27
|
+
// v2/v3 fallback (default export)
|
|
28
|
+
mmkv = new (m as any)({ id: 'eventlog-rn' });
|
|
29
|
+
|
|
30
|
+
} else {
|
|
31
|
+
throw new Error('MMKV module found but no valid constructor (createMMKV or class MMKV) found.');
|
|
32
|
+
}
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
35
|
+
throw new Error(
|
|
36
|
+
`[EventLog] Failed to initialize MMKV: ${errorMessage}.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const internalStorage: StorageAdapter = {
|
|
41
|
+
setItem: async (key: string, value: string): Promise<void> => {
|
|
42
|
+
mmkv.set(key, value);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
getItem: async (key: string): Promise<string | null> => {
|
|
46
|
+
return mmkv.getString(key) ?? null;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
removeItem: async (key: string): Promise<void> => {
|
|
50
|
+
// v4 uses remove(), v3 uses delete()
|
|
51
|
+
if (typeof mmkv.remove === 'function') {
|
|
52
|
+
mmkv.remove(key);
|
|
53
|
+
} else {
|
|
54
|
+
mmkv.delete(key);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for eventlog-rn/core
|
|
3
|
+
* Strict, immutable types following functional programming principles
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Storage adapter interface (AsyncStorage-compatible)
|
|
8
|
+
*/
|
|
9
|
+
export type StorageAdapter = Readonly<{
|
|
10
|
+
getItem: (key: string) => Promise<string | null>;
|
|
11
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
12
|
+
removeItem: (key: string) => Promise<void>;
|
|
13
|
+
}>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Event categories
|
|
17
|
+
*/
|
|
18
|
+
export type EventCategory = 'screen' | 'action' | 'network' | 'error' | 'log';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Log levels
|
|
22
|
+
*/
|
|
23
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Event context (attached to all events)
|
|
27
|
+
*/
|
|
28
|
+
export type EventContext = Readonly<{
|
|
29
|
+
readonly user?: Readonly<Record<string, unknown>>;
|
|
30
|
+
readonly deviceInfo?: Readonly<Record<string, unknown>>;
|
|
31
|
+
readonly [key: string]: unknown;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Immutable event structure
|
|
36
|
+
*/
|
|
37
|
+
export type Event = Readonly<{
|
|
38
|
+
readonly eventId: string;
|
|
39
|
+
readonly sessionId: string;
|
|
40
|
+
readonly seq: number;
|
|
41
|
+
readonly timestamp: number;
|
|
42
|
+
readonly timestampISO: string;
|
|
43
|
+
readonly timezone: string;
|
|
44
|
+
readonly category: EventCategory;
|
|
45
|
+
readonly payload: unknown;
|
|
46
|
+
readonly context: EventContext;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Session information
|
|
51
|
+
*/
|
|
52
|
+
export type Session = Readonly<{
|
|
53
|
+
readonly sessionId: string;
|
|
54
|
+
readonly sessionStart: number;
|
|
55
|
+
readonly startType: 'cold' | 'warm';
|
|
56
|
+
readonly seq: number;
|
|
57
|
+
readonly lastActivityTime: number;
|
|
58
|
+
}>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Feature configuration
|
|
62
|
+
*/
|
|
63
|
+
export type FeatureConfig = Readonly<{
|
|
64
|
+
readonly network?: Readonly<{
|
|
65
|
+
readonly enabled?: boolean;
|
|
66
|
+
readonly interceptFetch?: boolean;
|
|
67
|
+
readonly interceptAxios?: boolean;
|
|
68
|
+
readonly logRequestBody?: boolean;
|
|
69
|
+
readonly logResponseBody?: boolean;
|
|
70
|
+
readonly maxBodySize?: number;
|
|
71
|
+
readonly redactHeaders?: ReadonlyArray<string>;
|
|
72
|
+
}>;
|
|
73
|
+
readonly globalErrors?: Readonly<{
|
|
74
|
+
readonly enabled?: boolean;
|
|
75
|
+
}>;
|
|
76
|
+
}>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Tracker configuration
|
|
80
|
+
*/
|
|
81
|
+
export type EventLogConfig = Readonly<{
|
|
82
|
+
readonly autoDetect?: boolean;
|
|
83
|
+
readonly maxEvents?: number;
|
|
84
|
+
readonly maxAgeDays?: number;
|
|
85
|
+
readonly sessionTimeoutMinutes?: number;
|
|
86
|
+
readonly batchWriteDelayMs?: number;
|
|
87
|
+
readonly sanitize?: (event: Event) => Event;
|
|
88
|
+
readonly features?: FeatureConfig;
|
|
89
|
+
}>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Export options
|
|
93
|
+
*/
|
|
94
|
+
export type ExportOptions = Readonly<{
|
|
95
|
+
readonly mode: 'repro' | 'full';
|
|
96
|
+
}>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Network event payload
|
|
100
|
+
*/
|
|
101
|
+
export type NetworkEventPayload = Readonly<{
|
|
102
|
+
readonly url: string;
|
|
103
|
+
readonly method: string;
|
|
104
|
+
readonly status?: number;
|
|
105
|
+
readonly duration?: number;
|
|
106
|
+
readonly error?: string;
|
|
107
|
+
readonly requestHeaders?: Readonly<Record<string, string>>;
|
|
108
|
+
readonly responseHeaders?: Readonly<Record<string, string>>;
|
|
109
|
+
readonly requestBody?: unknown;
|
|
110
|
+
readonly responseBody?: unknown;
|
|
111
|
+
}>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Error event payload
|
|
115
|
+
*/
|
|
116
|
+
export type ErrorEventPayload = Readonly<{
|
|
117
|
+
readonly message: string;
|
|
118
|
+
readonly stack?: string;
|
|
119
|
+
readonly isFatal?: boolean;
|
|
120
|
+
readonly context?: unknown;
|
|
121
|
+
}>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Result type for operations that can fail
|
|
125
|
+
*/
|
|
126
|
+
export type Result<T, E = Error> =
|
|
127
|
+
| Readonly<{ readonly ok: true; readonly value: T }>
|
|
128
|
+
| Readonly<{ readonly ok: false; readonly error: E }>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Event query for filtering
|
|
132
|
+
*/
|
|
133
|
+
export type EventQuery = Readonly<{
|
|
134
|
+
readonly category?: ReadonlyArray<EventCategory>;
|
|
135
|
+
readonly timeRange?: Readonly<{ readonly start: number; readonly end: number }>;
|
|
136
|
+
readonly sessionId?: string;
|
|
137
|
+
readonly search?: string;
|
|
138
|
+
readonly limit?: number;
|
|
139
|
+
}>;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* EventLog API
|
|
143
|
+
*/
|
|
144
|
+
export type EventLog = Readonly<{
|
|
145
|
+
readonly init: (config?: Partial<EventLogConfig>) => Promise<Result<void>>;
|
|
146
|
+
readonly isReady: () => boolean;
|
|
147
|
+
readonly screen: (name: string, params?: unknown) => Result<void>;
|
|
148
|
+
readonly action: (name: string, data?: unknown) => Result<void>;
|
|
149
|
+
readonly log: (level: LogLevel, message: string, data?: unknown) => Result<void>;
|
|
150
|
+
readonly error: (error: unknown, context?: unknown) => Result<void>;
|
|
151
|
+
readonly setUser: (user: Readonly<Record<string, unknown>>) => void;
|
|
152
|
+
readonly setContext: (key: string, value: unknown) => void;
|
|
153
|
+
readonly setDeviceInfo: (info: Readonly<Record<string, unknown>>) => void;
|
|
154
|
+
readonly export: (options: ExportOptions) => Promise<Result<string>>;
|
|
155
|
+
readonly clear: () => Promise<Result<void>>;
|
|
156
|
+
readonly getEvents: () => Result<ReadonlyArray<Event>>;
|
|
157
|
+
readonly query: (query: EventQuery) => Result<ReadonlyArray<Event>>;
|
|
158
|
+
readonly network: (payload: NetworkEventPayload) => Result<void>;
|
|
159
|
+
}>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventItem component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { View, Text, TouchableOpacity } from 'react-native';
|
|
7
|
+
import type { EventItemProps } from './types';
|
|
8
|
+
import { CATEGORY_COLORS } from './types';
|
|
9
|
+
import { styles } from './styles';
|
|
10
|
+
|
|
11
|
+
export const EventItem: React.FC<EventItemProps> = ({ event, expanded, onPress }) => {
|
|
12
|
+
return (
|
|
13
|
+
<TouchableOpacity onPress={onPress} style={styles.eventItem}>
|
|
14
|
+
{/* Event Header */}
|
|
15
|
+
<View style={styles.eventHeader}>
|
|
16
|
+
<View
|
|
17
|
+
style={[
|
|
18
|
+
styles.categoryBadge,
|
|
19
|
+
{ backgroundColor: CATEGORY_COLORS[event.category] },
|
|
20
|
+
]}
|
|
21
|
+
>
|
|
22
|
+
<Text style={styles.categoryText}>{event.category}</Text>
|
|
23
|
+
</View>
|
|
24
|
+
<Text style={styles.timestamp}>
|
|
25
|
+
{new Date(event.timestamp).toLocaleTimeString()}
|
|
26
|
+
</Text>
|
|
27
|
+
</View>
|
|
28
|
+
|
|
29
|
+
{/* Event Summary */}
|
|
30
|
+
<Text style={styles.eventSummary} numberOfLines={expanded ? undefined : 1}>
|
|
31
|
+
{JSON.stringify(event.payload)}
|
|
32
|
+
</Text>
|
|
33
|
+
|
|
34
|
+
{/* Expanded Details */}
|
|
35
|
+
{expanded && (
|
|
36
|
+
<View style={styles.eventDetails}>
|
|
37
|
+
<Text style={styles.detailsLabel}>Session:</Text>
|
|
38
|
+
<Text style={styles.detailsText}>{event.sessionId}</Text>
|
|
39
|
+
|
|
40
|
+
<Text style={styles.detailsLabel}>Sequence:</Text>
|
|
41
|
+
<Text style={styles.detailsText}>{event.seq}</Text>
|
|
42
|
+
|
|
43
|
+
<Text style={styles.detailsLabel}>Timezone:</Text>
|
|
44
|
+
<Text style={styles.detailsText}>{event.timezone}</Text>
|
|
45
|
+
|
|
46
|
+
{event.context && Object.keys(event.context).length > 0 && (
|
|
47
|
+
<>
|
|
48
|
+
<Text style={styles.detailsLabel}>Context:</Text>
|
|
49
|
+
<Text style={styles.detailsText}>
|
|
50
|
+
{JSON.stringify(event.context, null, 2)}
|
|
51
|
+
</Text>
|
|
52
|
+
</>
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
<Text style={styles.detailsLabel}>Full Event:</Text>
|
|
56
|
+
<Text style={styles.detailsText}>
|
|
57
|
+
{JSON.stringify(event, null, 2)}
|
|
58
|
+
</Text>
|
|
59
|
+
</View>
|
|
60
|
+
)}
|
|
61
|
+
</TouchableOpacity>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventList component
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { FlatList, View, Text } from 'react-native';
|
|
7
|
+
import type { EventListProps } from './types';
|
|
8
|
+
import { EventItem } from './EventItem';
|
|
9
|
+
import { styles } from './styles';
|
|
10
|
+
import type { Event } from '../types';
|
|
11
|
+
|
|
12
|
+
export const EventList: React.FC<EventListProps> = ({ events, onEventPress, expandedId }) => {
|
|
13
|
+
const renderItem = ({ item }: { item: Event }) => (
|
|
14
|
+
<EventItem
|
|
15
|
+
event={item}
|
|
16
|
+
expanded={expandedId === item.eventId}
|
|
17
|
+
onPress={() => onEventPress(item.eventId)}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const keyExtractor = (item: Event) => item.eventId;
|
|
22
|
+
|
|
23
|
+
const renderEmpty = () => (
|
|
24
|
+
<View style={styles.emptyState}>
|
|
25
|
+
<Text style={styles.emptyText}>No events</Text>
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<FlatList
|
|
31
|
+
data={events}
|
|
32
|
+
renderItem={renderItem}
|
|
33
|
+
keyExtractor={keyExtractor}
|
|
34
|
+
ListEmptyComponent={renderEmpty}
|
|
35
|
+
style={styles.eventList}
|
|
36
|
+
initialNumToRender={20}
|
|
37
|
+
maxToRenderPerBatch={10}
|
|
38
|
+
windowSize={5}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
};
|