@fullevent/react 0.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/dist/index.d.mts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +183 -0
- package/dist/index.mjs +148 -0
- package/package.json +19 -0
- package/src/index.tsx +277 -0
- package/tsconfig.json +17 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface WideEvent {
|
|
4
|
+
request_id?: string;
|
|
5
|
+
trace_id?: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
method?: string;
|
|
8
|
+
path?: string;
|
|
9
|
+
status_code?: number;
|
|
10
|
+
duration_ms?: number;
|
|
11
|
+
outcome?: 'success' | 'error';
|
|
12
|
+
service?: string;
|
|
13
|
+
region?: string;
|
|
14
|
+
environment?: string;
|
|
15
|
+
user_id?: string;
|
|
16
|
+
user_email?: string;
|
|
17
|
+
user_plan?: string;
|
|
18
|
+
error?: {
|
|
19
|
+
type: string;
|
|
20
|
+
message: string;
|
|
21
|
+
stack?: string;
|
|
22
|
+
code?: string;
|
|
23
|
+
};
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
type SamplingConfig = {
|
|
27
|
+
/** Keep 10% of normal requests (0.0 - 1.0) */
|
|
28
|
+
defaultRate?: number;
|
|
29
|
+
/** Always keep error outcomes */
|
|
30
|
+
alwaysKeepErrors?: boolean;
|
|
31
|
+
/** Always keep slow requests (>ms) */
|
|
32
|
+
slowRequestThresholdMs?: number;
|
|
33
|
+
};
|
|
34
|
+
type FulleventConfig = {
|
|
35
|
+
apiUrl: string;
|
|
36
|
+
apiKey: string;
|
|
37
|
+
debug?: boolean;
|
|
38
|
+
/** Service name to tag all events with */
|
|
39
|
+
service?: string;
|
|
40
|
+
/** Environment (defaults to 'browser') */
|
|
41
|
+
environment?: string;
|
|
42
|
+
/** Sampling configuration */
|
|
43
|
+
sampling?: SamplingConfig;
|
|
44
|
+
};
|
|
45
|
+
interface EventBuilder {
|
|
46
|
+
/** Set any key-value pair on the event */
|
|
47
|
+
set: (key: string, value: unknown) => EventBuilder;
|
|
48
|
+
/** Set the user ID */
|
|
49
|
+
setUser: (userId: string) => EventBuilder;
|
|
50
|
+
/** Capture an error with structured details */
|
|
51
|
+
setError: (err: Error | {
|
|
52
|
+
type?: string;
|
|
53
|
+
message: string;
|
|
54
|
+
code?: string;
|
|
55
|
+
}) => EventBuilder;
|
|
56
|
+
/** Set the status code */
|
|
57
|
+
setStatus: (code: number) => EventBuilder;
|
|
58
|
+
/** Get the underlying event object */
|
|
59
|
+
getEvent: () => WideEvent;
|
|
60
|
+
/** Get the trace ID for this event (for correlating with backend) */
|
|
61
|
+
getTraceId: () => string;
|
|
62
|
+
/** Get headers to pass to fetch() for trace correlation */
|
|
63
|
+
getHeaders: () => Record<string, string>;
|
|
64
|
+
/** Emit the event to FullEvent API */
|
|
65
|
+
emit: () => Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
type FulleventContextType = {
|
|
68
|
+
/** Quick capture of a simple event */
|
|
69
|
+
capture: (event: string, properties?: Record<string, unknown>) => Promise<void>;
|
|
70
|
+
/** Create a wide event builder for accumulating context */
|
|
71
|
+
createEvent: (name: string) => EventBuilder;
|
|
72
|
+
/** Set global user context for all future events */
|
|
73
|
+
setUser: (userId: string) => void;
|
|
74
|
+
};
|
|
75
|
+
declare const FulleventProvider: React.FC<{
|
|
76
|
+
config: FulleventConfig;
|
|
77
|
+
children: React.ReactNode;
|
|
78
|
+
}>;
|
|
79
|
+
declare const useFullevent: () => FulleventContextType;
|
|
80
|
+
|
|
81
|
+
export { type EventBuilder, type FulleventConfig, FulleventProvider, type SamplingConfig, type WideEvent, useFullevent };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface WideEvent {
|
|
4
|
+
request_id?: string;
|
|
5
|
+
trace_id?: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
method?: string;
|
|
8
|
+
path?: string;
|
|
9
|
+
status_code?: number;
|
|
10
|
+
duration_ms?: number;
|
|
11
|
+
outcome?: 'success' | 'error';
|
|
12
|
+
service?: string;
|
|
13
|
+
region?: string;
|
|
14
|
+
environment?: string;
|
|
15
|
+
user_id?: string;
|
|
16
|
+
user_email?: string;
|
|
17
|
+
user_plan?: string;
|
|
18
|
+
error?: {
|
|
19
|
+
type: string;
|
|
20
|
+
message: string;
|
|
21
|
+
stack?: string;
|
|
22
|
+
code?: string;
|
|
23
|
+
};
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
type SamplingConfig = {
|
|
27
|
+
/** Keep 10% of normal requests (0.0 - 1.0) */
|
|
28
|
+
defaultRate?: number;
|
|
29
|
+
/** Always keep error outcomes */
|
|
30
|
+
alwaysKeepErrors?: boolean;
|
|
31
|
+
/** Always keep slow requests (>ms) */
|
|
32
|
+
slowRequestThresholdMs?: number;
|
|
33
|
+
};
|
|
34
|
+
type FulleventConfig = {
|
|
35
|
+
apiUrl: string;
|
|
36
|
+
apiKey: string;
|
|
37
|
+
debug?: boolean;
|
|
38
|
+
/** Service name to tag all events with */
|
|
39
|
+
service?: string;
|
|
40
|
+
/** Environment (defaults to 'browser') */
|
|
41
|
+
environment?: string;
|
|
42
|
+
/** Sampling configuration */
|
|
43
|
+
sampling?: SamplingConfig;
|
|
44
|
+
};
|
|
45
|
+
interface EventBuilder {
|
|
46
|
+
/** Set any key-value pair on the event */
|
|
47
|
+
set: (key: string, value: unknown) => EventBuilder;
|
|
48
|
+
/** Set the user ID */
|
|
49
|
+
setUser: (userId: string) => EventBuilder;
|
|
50
|
+
/** Capture an error with structured details */
|
|
51
|
+
setError: (err: Error | {
|
|
52
|
+
type?: string;
|
|
53
|
+
message: string;
|
|
54
|
+
code?: string;
|
|
55
|
+
}) => EventBuilder;
|
|
56
|
+
/** Set the status code */
|
|
57
|
+
setStatus: (code: number) => EventBuilder;
|
|
58
|
+
/** Get the underlying event object */
|
|
59
|
+
getEvent: () => WideEvent;
|
|
60
|
+
/** Get the trace ID for this event (for correlating with backend) */
|
|
61
|
+
getTraceId: () => string;
|
|
62
|
+
/** Get headers to pass to fetch() for trace correlation */
|
|
63
|
+
getHeaders: () => Record<string, string>;
|
|
64
|
+
/** Emit the event to FullEvent API */
|
|
65
|
+
emit: () => Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
type FulleventContextType = {
|
|
68
|
+
/** Quick capture of a simple event */
|
|
69
|
+
capture: (event: string, properties?: Record<string, unknown>) => Promise<void>;
|
|
70
|
+
/** Create a wide event builder for accumulating context */
|
|
71
|
+
createEvent: (name: string) => EventBuilder;
|
|
72
|
+
/** Set global user context for all future events */
|
|
73
|
+
setUser: (userId: string) => void;
|
|
74
|
+
};
|
|
75
|
+
declare const FulleventProvider: React.FC<{
|
|
76
|
+
config: FulleventConfig;
|
|
77
|
+
children: React.ReactNode;
|
|
78
|
+
}>;
|
|
79
|
+
declare const useFullevent: () => FulleventContextType;
|
|
80
|
+
|
|
81
|
+
export { type EventBuilder, type FulleventConfig, FulleventProvider, type SamplingConfig, type WideEvent, useFullevent };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/index.tsx
|
|
32
|
+
var index_exports = {};
|
|
33
|
+
__export(index_exports, {
|
|
34
|
+
FulleventProvider: () => FulleventProvider,
|
|
35
|
+
useFullevent: () => useFullevent
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
var import_react = __toESM(require("react"));
|
|
39
|
+
function shouldSample(event, config) {
|
|
40
|
+
const sampling = config ?? {};
|
|
41
|
+
const defaultRate = sampling.defaultRate ?? 1;
|
|
42
|
+
const alwaysKeepErrors = sampling.alwaysKeepErrors ?? true;
|
|
43
|
+
const slowThreshold = sampling.slowRequestThresholdMs ?? 2e3;
|
|
44
|
+
if (alwaysKeepErrors) {
|
|
45
|
+
if (event.outcome === "error") return true;
|
|
46
|
+
if (event.status_code && event.status_code >= 400) return true;
|
|
47
|
+
}
|
|
48
|
+
if (event.duration_ms && event.duration_ms > slowThreshold) return true;
|
|
49
|
+
if (event.trace_id) {
|
|
50
|
+
let hash = 5381;
|
|
51
|
+
const str = event.trace_id;
|
|
52
|
+
for (let i = 0; i < str.length; i++) {
|
|
53
|
+
hash = (hash << 5) + hash + str.charCodeAt(i);
|
|
54
|
+
}
|
|
55
|
+
const normalized = (hash >>> 0) % 1e4 / 1e4;
|
|
56
|
+
return normalized < defaultRate;
|
|
57
|
+
}
|
|
58
|
+
return Math.random() < defaultRate;
|
|
59
|
+
}
|
|
60
|
+
function createEventBuilder(name, sendFn, baseContext) {
|
|
61
|
+
const startTime = Date.now();
|
|
62
|
+
const traceId = crypto.randomUUID();
|
|
63
|
+
const event = {
|
|
64
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65
|
+
trace_id: traceId,
|
|
66
|
+
request_id: traceId,
|
|
67
|
+
...baseContext
|
|
68
|
+
};
|
|
69
|
+
const builder = {
|
|
70
|
+
set(key, value) {
|
|
71
|
+
event[key] = value;
|
|
72
|
+
return builder;
|
|
73
|
+
},
|
|
74
|
+
setUser(userId) {
|
|
75
|
+
event.user_id = userId;
|
|
76
|
+
return builder;
|
|
77
|
+
},
|
|
78
|
+
setError(err) {
|
|
79
|
+
event.outcome = "error";
|
|
80
|
+
if (err instanceof Error) {
|
|
81
|
+
event.error = {
|
|
82
|
+
type: err.name,
|
|
83
|
+
message: err.message,
|
|
84
|
+
stack: err.stack
|
|
85
|
+
};
|
|
86
|
+
} else {
|
|
87
|
+
event.error = {
|
|
88
|
+
type: err.type || "Error",
|
|
89
|
+
message: err.message,
|
|
90
|
+
code: err.code
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return builder;
|
|
94
|
+
},
|
|
95
|
+
setStatus(code) {
|
|
96
|
+
event.status_code = code;
|
|
97
|
+
event.outcome = code >= 400 ? "error" : "success";
|
|
98
|
+
return builder;
|
|
99
|
+
},
|
|
100
|
+
getEvent() {
|
|
101
|
+
return event;
|
|
102
|
+
},
|
|
103
|
+
getTraceId() {
|
|
104
|
+
return traceId;
|
|
105
|
+
},
|
|
106
|
+
getHeaders() {
|
|
107
|
+
return {
|
|
108
|
+
"x-fullevent-trace-id": traceId
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
async emit() {
|
|
112
|
+
event.duration_ms = Date.now() - startTime;
|
|
113
|
+
if (!event.outcome) {
|
|
114
|
+
event.outcome = "success";
|
|
115
|
+
}
|
|
116
|
+
await sendFn(name, event, event);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
return builder;
|
|
120
|
+
}
|
|
121
|
+
var FulleventContext = (0, import_react.createContext)(void 0);
|
|
122
|
+
var FulleventProvider = ({ config, children }) => {
|
|
123
|
+
let globalUserId;
|
|
124
|
+
const baseContext = {
|
|
125
|
+
service: config.service,
|
|
126
|
+
environment: config.environment || "browser"
|
|
127
|
+
};
|
|
128
|
+
const capture = async (event, properties, wideEvent) => {
|
|
129
|
+
if (wideEvent && !shouldSample(wideEvent, config.sampling)) {
|
|
130
|
+
if (config.debug) {
|
|
131
|
+
console.log(`[Fullevent] Sampling dropped event: ${event}`);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (config.debug) {
|
|
136
|
+
console.log(`[Fullevent] Capturing event: ${event}`, properties);
|
|
137
|
+
}
|
|
138
|
+
const payload = {
|
|
139
|
+
...baseContext,
|
|
140
|
+
...properties,
|
|
141
|
+
user_id: properties?.user_id || globalUserId
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
await fetch(`${config.apiUrl}/ingest`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: {
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
"Authorization": `Bearer ${config.apiKey}`
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
event,
|
|
152
|
+
properties: payload,
|
|
153
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
154
|
+
})
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error("[Fullevent] Failed to capture event:", error);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const createEvent = (name) => {
|
|
161
|
+
const builder = createEventBuilder(name, capture, {
|
|
162
|
+
...baseContext,
|
|
163
|
+
user_id: globalUserId
|
|
164
|
+
});
|
|
165
|
+
return builder;
|
|
166
|
+
};
|
|
167
|
+
const setUser = (userId) => {
|
|
168
|
+
globalUserId = userId;
|
|
169
|
+
};
|
|
170
|
+
return /* @__PURE__ */ import_react.default.createElement(FulleventContext.Provider, { value: { capture, createEvent, setUser } }, children);
|
|
171
|
+
};
|
|
172
|
+
var useFullevent = () => {
|
|
173
|
+
const context = (0, import_react.useContext)(FulleventContext);
|
|
174
|
+
if (!context) {
|
|
175
|
+
throw new Error("useFullevent must be used within a FulleventProvider");
|
|
176
|
+
}
|
|
177
|
+
return context;
|
|
178
|
+
};
|
|
179
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
180
|
+
0 && (module.exports = {
|
|
181
|
+
FulleventProvider,
|
|
182
|
+
useFullevent
|
|
183
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/index.tsx
|
|
4
|
+
import React, { createContext, useContext } from "react";
|
|
5
|
+
function shouldSample(event, config) {
|
|
6
|
+
const sampling = config ?? {};
|
|
7
|
+
const defaultRate = sampling.defaultRate ?? 1;
|
|
8
|
+
const alwaysKeepErrors = sampling.alwaysKeepErrors ?? true;
|
|
9
|
+
const slowThreshold = sampling.slowRequestThresholdMs ?? 2e3;
|
|
10
|
+
if (alwaysKeepErrors) {
|
|
11
|
+
if (event.outcome === "error") return true;
|
|
12
|
+
if (event.status_code && event.status_code >= 400) return true;
|
|
13
|
+
}
|
|
14
|
+
if (event.duration_ms && event.duration_ms > slowThreshold) return true;
|
|
15
|
+
if (event.trace_id) {
|
|
16
|
+
let hash = 5381;
|
|
17
|
+
const str = event.trace_id;
|
|
18
|
+
for (let i = 0; i < str.length; i++) {
|
|
19
|
+
hash = (hash << 5) + hash + str.charCodeAt(i);
|
|
20
|
+
}
|
|
21
|
+
const normalized = (hash >>> 0) % 1e4 / 1e4;
|
|
22
|
+
return normalized < defaultRate;
|
|
23
|
+
}
|
|
24
|
+
return Math.random() < defaultRate;
|
|
25
|
+
}
|
|
26
|
+
function createEventBuilder(name, sendFn, baseContext) {
|
|
27
|
+
const startTime = Date.now();
|
|
28
|
+
const traceId = crypto.randomUUID();
|
|
29
|
+
const event = {
|
|
30
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31
|
+
trace_id: traceId,
|
|
32
|
+
request_id: traceId,
|
|
33
|
+
...baseContext
|
|
34
|
+
};
|
|
35
|
+
const builder = {
|
|
36
|
+
set(key, value) {
|
|
37
|
+
event[key] = value;
|
|
38
|
+
return builder;
|
|
39
|
+
},
|
|
40
|
+
setUser(userId) {
|
|
41
|
+
event.user_id = userId;
|
|
42
|
+
return builder;
|
|
43
|
+
},
|
|
44
|
+
setError(err) {
|
|
45
|
+
event.outcome = "error";
|
|
46
|
+
if (err instanceof Error) {
|
|
47
|
+
event.error = {
|
|
48
|
+
type: err.name,
|
|
49
|
+
message: err.message,
|
|
50
|
+
stack: err.stack
|
|
51
|
+
};
|
|
52
|
+
} else {
|
|
53
|
+
event.error = {
|
|
54
|
+
type: err.type || "Error",
|
|
55
|
+
message: err.message,
|
|
56
|
+
code: err.code
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return builder;
|
|
60
|
+
},
|
|
61
|
+
setStatus(code) {
|
|
62
|
+
event.status_code = code;
|
|
63
|
+
event.outcome = code >= 400 ? "error" : "success";
|
|
64
|
+
return builder;
|
|
65
|
+
},
|
|
66
|
+
getEvent() {
|
|
67
|
+
return event;
|
|
68
|
+
},
|
|
69
|
+
getTraceId() {
|
|
70
|
+
return traceId;
|
|
71
|
+
},
|
|
72
|
+
getHeaders() {
|
|
73
|
+
return {
|
|
74
|
+
"x-fullevent-trace-id": traceId
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
async emit() {
|
|
78
|
+
event.duration_ms = Date.now() - startTime;
|
|
79
|
+
if (!event.outcome) {
|
|
80
|
+
event.outcome = "success";
|
|
81
|
+
}
|
|
82
|
+
await sendFn(name, event, event);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return builder;
|
|
86
|
+
}
|
|
87
|
+
var FulleventContext = createContext(void 0);
|
|
88
|
+
var FulleventProvider = ({ config, children }) => {
|
|
89
|
+
let globalUserId;
|
|
90
|
+
const baseContext = {
|
|
91
|
+
service: config.service,
|
|
92
|
+
environment: config.environment || "browser"
|
|
93
|
+
};
|
|
94
|
+
const capture = async (event, properties, wideEvent) => {
|
|
95
|
+
if (wideEvent && !shouldSample(wideEvent, config.sampling)) {
|
|
96
|
+
if (config.debug) {
|
|
97
|
+
console.log(`[Fullevent] Sampling dropped event: ${event}`);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (config.debug) {
|
|
102
|
+
console.log(`[Fullevent] Capturing event: ${event}`, properties);
|
|
103
|
+
}
|
|
104
|
+
const payload = {
|
|
105
|
+
...baseContext,
|
|
106
|
+
...properties,
|
|
107
|
+
user_id: properties?.user_id || globalUserId
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
await fetch(`${config.apiUrl}/ingest`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: {
|
|
113
|
+
"Content-Type": "application/json",
|
|
114
|
+
"Authorization": `Bearer ${config.apiKey}`
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
event,
|
|
118
|
+
properties: payload,
|
|
119
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
120
|
+
})
|
|
121
|
+
});
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error("[Fullevent] Failed to capture event:", error);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const createEvent = (name) => {
|
|
127
|
+
const builder = createEventBuilder(name, capture, {
|
|
128
|
+
...baseContext,
|
|
129
|
+
user_id: globalUserId
|
|
130
|
+
});
|
|
131
|
+
return builder;
|
|
132
|
+
};
|
|
133
|
+
const setUser = (userId) => {
|
|
134
|
+
globalUserId = userId;
|
|
135
|
+
};
|
|
136
|
+
return /* @__PURE__ */ React.createElement(FulleventContext.Provider, { value: { capture, createEvent, setUser } }, children);
|
|
137
|
+
};
|
|
138
|
+
var useFullevent = () => {
|
|
139
|
+
const context = useContext(FulleventContext);
|
|
140
|
+
if (!context) {
|
|
141
|
+
throw new Error("useFullevent must be used within a FulleventProvider");
|
|
142
|
+
}
|
|
143
|
+
return context;
|
|
144
|
+
};
|
|
145
|
+
export {
|
|
146
|
+
FulleventProvider,
|
|
147
|
+
useFullevent
|
|
148
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fullevent/react",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"module": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsup src/index.tsx --format cjs,esm --dts",
|
|
9
|
+
"dev": "tsup src/index.tsx --format cjs,esm --dts --watch"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"react": "^19.0.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"tsup": "^8.0.0",
|
|
16
|
+
"typescript": "^5.0.0",
|
|
17
|
+
"@types/react": "^19.0.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { createContext, useContext } from 'react';
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// Types - Match Node SDK
|
|
6
|
+
// ============================================================
|
|
7
|
+
|
|
8
|
+
export interface WideEvent {
|
|
9
|
+
// Core Request Context
|
|
10
|
+
request_id?: string;
|
|
11
|
+
trace_id?: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
method?: string;
|
|
14
|
+
path?: string;
|
|
15
|
+
status_code?: number;
|
|
16
|
+
duration_ms?: number;
|
|
17
|
+
outcome?: 'success' | 'error';
|
|
18
|
+
|
|
19
|
+
// Infrastructure Context
|
|
20
|
+
service?: string;
|
|
21
|
+
region?: string;
|
|
22
|
+
environment?: string;
|
|
23
|
+
|
|
24
|
+
// User Context
|
|
25
|
+
user_id?: string;
|
|
26
|
+
user_email?: string;
|
|
27
|
+
user_plan?: string;
|
|
28
|
+
|
|
29
|
+
// Error Details
|
|
30
|
+
error?: {
|
|
31
|
+
type: string;
|
|
32
|
+
message: string;
|
|
33
|
+
stack?: string;
|
|
34
|
+
code?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Dynamic Business Context
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SamplingConfig = {
|
|
42
|
+
/** Keep 10% of normal requests (0.0 - 1.0) */
|
|
43
|
+
defaultRate?: number;
|
|
44
|
+
/** Always keep error outcomes */
|
|
45
|
+
alwaysKeepErrors?: boolean;
|
|
46
|
+
/** Always keep slow requests (>ms) */
|
|
47
|
+
slowRequestThresholdMs?: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type FulleventConfig = {
|
|
51
|
+
apiUrl: string;
|
|
52
|
+
apiKey: string;
|
|
53
|
+
debug?: boolean;
|
|
54
|
+
/** Service name to tag all events with */
|
|
55
|
+
service?: string;
|
|
56
|
+
/** Environment (defaults to 'browser') */
|
|
57
|
+
environment?: string;
|
|
58
|
+
/** Sampling configuration */
|
|
59
|
+
sampling?: SamplingConfig;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Helper for Consistent Sampling
|
|
63
|
+
function shouldSample(event: WideEvent, config?: SamplingConfig): boolean {
|
|
64
|
+
const sampling = config ?? {};
|
|
65
|
+
const defaultRate = sampling.defaultRate ?? 1.0;
|
|
66
|
+
const alwaysKeepErrors = sampling.alwaysKeepErrors ?? true;
|
|
67
|
+
const slowThreshold = sampling.slowRequestThresholdMs ?? 2000;
|
|
68
|
+
|
|
69
|
+
// Always keep errors
|
|
70
|
+
if (alwaysKeepErrors) {
|
|
71
|
+
if (event.outcome === 'error') return true;
|
|
72
|
+
if (event.status_code && event.status_code >= 400) return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Always keep slow requests
|
|
76
|
+
if (event.duration_ms && event.duration_ms > slowThreshold) return true;
|
|
77
|
+
|
|
78
|
+
// Consistent Sampling based on Trace ID
|
|
79
|
+
if (event.trace_id) {
|
|
80
|
+
let hash = 5381;
|
|
81
|
+
const str = event.trace_id;
|
|
82
|
+
for (let i = 0; i < str.length; i++) {
|
|
83
|
+
hash = ((hash << 5) + hash) + str.charCodeAt(i);
|
|
84
|
+
}
|
|
85
|
+
const normalized = (hash >>> 0) % 10000 / 10000;
|
|
86
|
+
return normalized < defaultRate;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return Math.random() < defaultRate;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================
|
|
93
|
+
// Event Builder - Match Node SDK API
|
|
94
|
+
// ============================================================
|
|
95
|
+
|
|
96
|
+
export interface EventBuilder {
|
|
97
|
+
/** Set any key-value pair on the event */
|
|
98
|
+
set: (key: string, value: unknown) => EventBuilder;
|
|
99
|
+
/** Set the user ID */
|
|
100
|
+
setUser: (userId: string) => EventBuilder;
|
|
101
|
+
/** Capture an error with structured details */
|
|
102
|
+
setError: (err: Error | { type?: string; message: string; code?: string }) => EventBuilder;
|
|
103
|
+
/** Set the status code */
|
|
104
|
+
setStatus: (code: number) => EventBuilder;
|
|
105
|
+
/** Get the underlying event object */
|
|
106
|
+
getEvent: () => WideEvent;
|
|
107
|
+
/** Get the trace ID for this event (for correlating with backend) */
|
|
108
|
+
getTraceId: () => string;
|
|
109
|
+
/** Get headers to pass to fetch() for trace correlation */
|
|
110
|
+
getHeaders: () => Record<string, string>;
|
|
111
|
+
/** Emit the event to FullEvent API */
|
|
112
|
+
emit: () => Promise<void>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function createEventBuilder(
|
|
116
|
+
name: string,
|
|
117
|
+
sendFn: (event: string, properties: Record<string, unknown>, wideEvent: WideEvent) => Promise<void>,
|
|
118
|
+
baseContext: Partial<WideEvent>
|
|
119
|
+
): EventBuilder {
|
|
120
|
+
const startTime = Date.now();
|
|
121
|
+
// Generate a unique trace ID for this event
|
|
122
|
+
const traceId = crypto.randomUUID();
|
|
123
|
+
|
|
124
|
+
const event: WideEvent = {
|
|
125
|
+
timestamp: new Date().toISOString(),
|
|
126
|
+
trace_id: traceId,
|
|
127
|
+
request_id: traceId,
|
|
128
|
+
...baseContext,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const builder: EventBuilder = {
|
|
132
|
+
set(key: string, value: unknown) {
|
|
133
|
+
event[key] = value;
|
|
134
|
+
return builder;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
setUser(userId: string) {
|
|
138
|
+
event.user_id = userId;
|
|
139
|
+
return builder;
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
setError(err: Error | { type?: string; message: string; code?: string }) {
|
|
143
|
+
event.outcome = 'error';
|
|
144
|
+
if (err instanceof Error) {
|
|
145
|
+
event.error = {
|
|
146
|
+
type: err.name,
|
|
147
|
+
message: err.message,
|
|
148
|
+
stack: err.stack,
|
|
149
|
+
};
|
|
150
|
+
} else {
|
|
151
|
+
event.error = {
|
|
152
|
+
type: err.type || 'Error',
|
|
153
|
+
message: err.message,
|
|
154
|
+
code: err.code,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return builder;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
setStatus(code: number) {
|
|
161
|
+
event.status_code = code;
|
|
162
|
+
event.outcome = code >= 400 ? 'error' : 'success';
|
|
163
|
+
return builder;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
getEvent() {
|
|
167
|
+
return event;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
getTraceId() {
|
|
171
|
+
return traceId;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
getHeaders() {
|
|
175
|
+
return {
|
|
176
|
+
'x-fullevent-trace-id': traceId,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async emit() {
|
|
181
|
+
event.duration_ms = Date.now() - startTime;
|
|
182
|
+
if (!event.outcome) {
|
|
183
|
+
event.outcome = 'success';
|
|
184
|
+
}
|
|
185
|
+
await sendFn(name, event as Record<string, unknown>, event);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return builder;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ============================================================
|
|
193
|
+
// Context & Provider
|
|
194
|
+
// ============================================================
|
|
195
|
+
|
|
196
|
+
type FulleventContextType = {
|
|
197
|
+
/** Quick capture of a simple event */
|
|
198
|
+
capture: (event: string, properties?: Record<string, unknown>) => Promise<void>;
|
|
199
|
+
/** Create a wide event builder for accumulating context */
|
|
200
|
+
createEvent: (name: string) => EventBuilder;
|
|
201
|
+
/** Set global user context for all future events */
|
|
202
|
+
setUser: (userId: string) => void;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const FulleventContext = createContext<FulleventContextType | undefined>(undefined);
|
|
206
|
+
|
|
207
|
+
export const FulleventProvider: React.FC<{ config: FulleventConfig; children: React.ReactNode }> = ({ config, children }) => {
|
|
208
|
+
// Global user context that gets added to all events
|
|
209
|
+
let globalUserId: string | undefined;
|
|
210
|
+
|
|
211
|
+
const baseContext: Partial<WideEvent> = {
|
|
212
|
+
service: config.service,
|
|
213
|
+
environment: config.environment || 'browser',
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const capture = async (event: string, properties?: Record<string, unknown>, wideEvent?: WideEvent) => {
|
|
217
|
+
// If it's a wide event (via createEvent), check sampling
|
|
218
|
+
if (wideEvent && !shouldSample(wideEvent, config.sampling)) {
|
|
219
|
+
if (config.debug) {
|
|
220
|
+
console.log(`[Fullevent] Sampling dropped event: ${event}`);
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (config.debug) {
|
|
225
|
+
console.log(`[Fullevent] Capturing event: ${event}`, properties);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const payload = {
|
|
229
|
+
...baseContext,
|
|
230
|
+
...properties,
|
|
231
|
+
user_id: properties?.user_id || globalUserId,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await fetch(`${config.apiUrl}/ingest`, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: {
|
|
238
|
+
'Content-Type': 'application/json',
|
|
239
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
240
|
+
},
|
|
241
|
+
body: JSON.stringify({
|
|
242
|
+
event,
|
|
243
|
+
properties: payload,
|
|
244
|
+
timestamp: new Date().toISOString(),
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error('[Fullevent] Failed to capture event:', error);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const createEvent = (name: string): EventBuilder => {
|
|
253
|
+
const builder = createEventBuilder(name, capture, {
|
|
254
|
+
...baseContext,
|
|
255
|
+
user_id: globalUserId,
|
|
256
|
+
});
|
|
257
|
+
return builder;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const setUser = (userId: string) => {
|
|
261
|
+
globalUserId = userId;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<FulleventContext.Provider value={{ capture, createEvent, setUser }}>
|
|
266
|
+
{children}
|
|
267
|
+
</FulleventContext.Provider>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export const useFullevent = () => {
|
|
272
|
+
const context = useContext(FulleventContext);
|
|
273
|
+
if (!context) {
|
|
274
|
+
throw new Error('useFullevent must be used within a FulleventProvider');
|
|
275
|
+
}
|
|
276
|
+
return context;
|
|
277
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"jsx": "react",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"outDir": "./dist"
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"src/**/*"
|
|
16
|
+
]
|
|
17
|
+
}
|