@trakoo/emitkit 0.0.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 trakoo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @trakoo/emitkit
2
+
3
+ EmitKit server provider for [trakoo](https://www.npmjs.com/package/trakoo), the typed, provider-agnostic analytics library.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add trakoo @trakoo/emitkit @emitkit/js@next
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ `appEvents` is your event registry, created with `defineEvents()` from `trakoo`.
14
+
15
+ ```typescript title="lib/server-analytics.ts"
16
+ import { createServerAnalytics } from 'trakoo/server';
17
+ import { EmitKitServerProvider } from '@trakoo/emitkit/server';
18
+ import { appEvents } from './events';
19
+
20
+ export const serverAnalytics = createServerAnalytics({
21
+ events: appEvents,
22
+ providers: [
23
+ new EmitKitServerProvider({
24
+ apiKey: process.env.EMITKIT_API_KEY!,
25
+ channelName: 'general',
26
+ categoryChannelMap: {
27
+ user: 'user-activity',
28
+ conversion: 'revenue'
29
+ }
30
+ })
31
+ ]
32
+ });
33
+ ```
34
+
35
+ EmitKit is server-only. To send browser events to it, forward them through trakoo's [Proxy provider](https://trakoo.co/docs/providers/proxy).
36
+
37
+ The provider also sends page views, as silent events. EmitKit allows 100 requests per minute per API key by default, so if you only want explicit events in your feeds, register it as `{ provider: new EmitKitServerProvider({ ... }), exclude: ['pageView'] }`.
38
+
39
+ ## Documentation
40
+
41
+ https://trakoo.co/docs/providers/emitkit
@@ -0,0 +1,104 @@
1
+ import { BaseAnalyticsProvider, type BaseEvent, type EventContext } from "trakoo";
2
+ /**
3
+ * Configuration for EmitKit server provider
4
+ */
5
+ export interface EmitKitServerConfig {
6
+ /**
7
+ * Your EmitKit API key (starts with emitkit_)
8
+ */
9
+ apiKey: string;
10
+ /**
11
+ * Request timeout in milliseconds. A request that fails or times out is not
12
+ * retried.
13
+ * @default 5000
14
+ */
15
+ timeout?: number;
16
+ /**
17
+ * Default channel name for events
18
+ * @default 'general'
19
+ */
20
+ channelName?: string;
21
+ /**
22
+ * Map event categories to specific EmitKit channels.
23
+ * Allows automatic routing of events to appropriate channels based on category.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * {
28
+ * 'user': 'user-activity',
29
+ * 'engagement': 'product-usage',
30
+ * 'error': 'alerts',
31
+ * 'conversion': 'revenue'
32
+ * }
33
+ * ```
34
+ *
35
+ * Channel resolution priority:
36
+ * 1. Event property `__emitkit_channel` (highest priority)
37
+ * 2. Category mapping via `categoryChannelMap`
38
+ * 3. Default `channelName` (fallback, default: 'general')
39
+ */
40
+ categoryChannelMap?: Record<string, string>;
41
+ /**
42
+ * Send notification for events
43
+ * @default true
44
+ */
45
+ notify?: boolean;
46
+ /**
47
+ * Display style for events
48
+ * @default 'notification'
49
+ */
50
+ displayAs?: "message" | "notification";
51
+ /**
52
+ * Enable debug logging
53
+ */
54
+ debug?: boolean;
55
+ /**
56
+ * Enable/disable the provider
57
+ */
58
+ enabled?: boolean;
59
+ }
60
+ export declare class EmitKitServerProvider extends BaseAnalyticsProvider {
61
+ name: string;
62
+ private client?;
63
+ private initialized;
64
+ private config;
65
+ constructor(config: EmitKitServerConfig);
66
+ initialize(): Promise<void>;
67
+ identify(userId: string, traits?: Record<string, unknown>): Promise<void>;
68
+ track(event: BaseEvent, context?: EventContext): Promise<void>;
69
+ pageView(properties?: Record<string, unknown>, context?: EventContext): Promise<void>;
70
+ reset(): Promise<void>;
71
+ shutdown(): Promise<void>;
72
+ /**
73
+ * Request context attached to event metadata. EmitKit shows metadata in
74
+ * team feeds and notifications, so the visitor IP address (which proxy
75
+ * ingestion adds to `device`) is never forwarded.
76
+ */
77
+ private getContextMetadata;
78
+ /**
79
+ * Describe an SDK failure for logs. EmitKit errors add their error code,
80
+ * HTTP status, and request id, which tell auth, validation, and rate-limit
81
+ * failures apart; the message and response body are never logged.
82
+ */
83
+ private describeError;
84
+ /**
85
+ * Format event action into a human-readable title
86
+ * Converts: "user_signed_up" -> "User Signed Up"
87
+ */
88
+ private formatEventTitle;
89
+ /**
90
+ * Generate a description for the event
91
+ */
92
+ private getEventDescription;
93
+ /**
94
+ * Get an appropriate icon for the event category
95
+ */
96
+ private getEventIcon;
97
+ /**
98
+ * Resolve the channel name for an event based on priority:
99
+ * 1. Event property __emitkit_channel (highest priority)
100
+ * 2. Category mapping via categoryChannelMap
101
+ * 3. Default channelName (fallback, default: 'general')
102
+ */
103
+ private resolveChannelName;
104
+ }
package/dist/server.js ADDED
@@ -0,0 +1,314 @@
1
+ import { BaseAnalyticsProvider, } from "trakoo";
2
+ const DEFAULT_TIMEOUT = 5000;
3
+ /** Identifies trakoo as the sender of each event in EmitKit. */
4
+ const EVENT_SOURCE = "trakoo";
5
+ export class EmitKitServerProvider extends BaseAnalyticsProvider {
6
+ name = "EmitKit-Server";
7
+ client;
8
+ initialized = false;
9
+ config;
10
+ constructor(config) {
11
+ super({ debug: config.debug, enabled: config.enabled });
12
+ this.config = config;
13
+ }
14
+ async initialize() {
15
+ if (!this.isEnabled())
16
+ return;
17
+ if (this.initialized)
18
+ return;
19
+ // Validate config has required fields
20
+ if (!this.config.apiKey || typeof this.config.apiKey !== "string") {
21
+ throw new Error("EmitKit requires an apiKey");
22
+ }
23
+ if (!this.config.apiKey.startsWith("emitkit_")) {
24
+ console.warn("[EmitKit-Server] API key should start with 'emitkit_'. Double check your configuration.");
25
+ }
26
+ try {
27
+ // Dynamically import the EmitKit SDK
28
+ const { EmitKit } = await import("@emitkit/js");
29
+ this.client = new EmitKit({
30
+ apiKey: this.config.apiKey,
31
+ timeout: this.config.timeout ?? DEFAULT_TIMEOUT,
32
+ // The SDK's retries would hold a caller for several timeouts, so a
33
+ // failed request is reported once `timeout` has passed instead.
34
+ maxRetries: 0,
35
+ });
36
+ this.initialized = true;
37
+ this.log("Initialized successfully");
38
+ }
39
+ catch (error) {
40
+ console.error(`[EmitKit-Server] Failed to initialize (${this.getErrorClass(error)})`);
41
+ throw error;
42
+ }
43
+ }
44
+ async identify(userId, traits) {
45
+ if (!this.isEnabled() || !this.initialized || !this.client)
46
+ return;
47
+ // Extract email from traits; EmitKit rejects non-string aliases.
48
+ const email = typeof traits?.email === "string" && traits.email ? traits.email : userId;
49
+ // Build aliases array - EmitKit supports multiple identifiers
50
+ const aliases = [];
51
+ // Add userId as primary alias
52
+ if (userId) {
53
+ aliases.push(userId);
54
+ }
55
+ // Add email if different from userId
56
+ if (email && email !== userId) {
57
+ aliases.push(email);
58
+ }
59
+ // Add any custom alias fields from traits
60
+ if (traits?.username && typeof traits.username === "string") {
61
+ aliases.push(traits.username);
62
+ }
63
+ try {
64
+ await this.client.identify({
65
+ userId,
66
+ // EmitKit merges these into the stored properties.
67
+ ...(traits && { properties: asJsonObject(traits) }),
68
+ aliases: aliases.length > 0 ? aliases : undefined,
69
+ });
70
+ this.log("Identified user");
71
+ }
72
+ catch (error) {
73
+ console.error(`[EmitKit-Server] Failed to identify user (${this.describeError(error)})`);
74
+ }
75
+ }
76
+ async track(event, context) {
77
+ if (!this.isEnabled() || !this.initialized || !this.client)
78
+ return;
79
+ // Server providers use only identity supplied on the current call.
80
+ const userId = context?.user?.email || context?.user?.userId || event.userId;
81
+ // Generate event title from action (convert snake_case to Title Case)
82
+ const title = this.formatEventTitle(event.action);
83
+ // Build metadata from event properties and context
84
+ // Strip __emitkit_channel from properties as it's internal routing metadata
85
+ const { __emitkit_channel, ...cleanProperties } = event.properties || {};
86
+ const metadata = {
87
+ ...cleanProperties,
88
+ category: event.category,
89
+ timestamp: event.timestamp || Date.now(),
90
+ ...(event.sessionId && { sessionId: event.sessionId }),
91
+ ...this.getContextMetadata(context),
92
+ };
93
+ // Extract tags from category
94
+ const tags = [];
95
+ if (event.category) {
96
+ tags.push(event.category);
97
+ }
98
+ // Add any custom tags from properties
99
+ if (cleanProperties?.tags &&
100
+ Array.isArray(cleanProperties.tags) &&
101
+ cleanProperties.tags.every((t) => typeof t === "string")) {
102
+ tags.push(...cleanProperties.tags);
103
+ }
104
+ const uniqueTags = [...new Set(tags)];
105
+ // Determine channel name using resolution logic
106
+ const channelName = this.resolveChannelName(event);
107
+ try {
108
+ await this.client.events.create({
109
+ channelName,
110
+ title,
111
+ description: this.getEventDescription(event, context),
112
+ icon: this.getEventIcon(event.category),
113
+ tags: uniqueTags.length > 0 ? uniqueTags : undefined,
114
+ metadata: asJsonObject(metadata),
115
+ userId: userId || null,
116
+ notify: this.config.notify ?? true,
117
+ displayAs: this.config.displayAs || "notification",
118
+ source: EVENT_SOURCE,
119
+ });
120
+ this.log("Tracked event");
121
+ }
122
+ catch (error) {
123
+ console.error(`[EmitKit-Server] Failed to track event (${this.describeError(error)})`);
124
+ throw error;
125
+ }
126
+ }
127
+ async pageView(properties, context) {
128
+ if (!this.isEnabled() || !this.initialized || !this.client)
129
+ return;
130
+ // Page views may only use identity supplied in the current context.
131
+ const userId = context?.user?.email || context?.user?.userId;
132
+ // Strip __emitkit_channel from properties if present
133
+ const { __emitkit_channel, ...cleanProperties } = properties || {};
134
+ // Build page view metadata
135
+ const metadata = {
136
+ ...cleanProperties,
137
+ date: new Date().toISOString(),
138
+ ...this.getContextMetadata(context),
139
+ };
140
+ // Create a synthetic event for channel resolution
141
+ // Page views use 'navigation' category
142
+ const syntheticEvent = {
143
+ action: "page_view",
144
+ category: "navigation",
145
+ properties: properties || {},
146
+ };
147
+ // Determine channel name using resolution logic
148
+ const channelName = this.resolveChannelName(syntheticEvent);
149
+ try {
150
+ await this.client.events.create({
151
+ channelName,
152
+ title: "Page View",
153
+ description: context?.page?.path || "User viewed a page",
154
+ icon: "👁️",
155
+ tags: ["page_view", "navigation"],
156
+ metadata: asJsonObject(metadata),
157
+ userId: userId || null,
158
+ notify: false, // Don't notify for page views by default
159
+ displayAs: "message",
160
+ source: EVENT_SOURCE,
161
+ });
162
+ this.log("Tracked page view");
163
+ }
164
+ catch (error) {
165
+ console.error(`[EmitKit-Server] Failed to track page view (${this.describeError(error)})`);
166
+ }
167
+ }
168
+ async reset() {
169
+ if (!this.isEnabled() || !this.initialized || !this.client)
170
+ return;
171
+ this.log("Reset called; server provider has no retained identity");
172
+ }
173
+ async shutdown() {
174
+ // EmitKit SDK doesn't require explicit shutdown
175
+ // Events are sent immediately (not batched)
176
+ this.client = undefined;
177
+ this.initialized = false;
178
+ this.log("Shutdown complete");
179
+ }
180
+ // ============================================================================
181
+ // Helper Methods
182
+ // ============================================================================
183
+ /**
184
+ * Request context attached to event metadata. EmitKit shows metadata in
185
+ * team feeds and notifications, so the visitor IP address (which proxy
186
+ * ingestion adds to `device`) is never forwarded.
187
+ */
188
+ getContextMetadata(context) {
189
+ const device = context?.device && withoutIp(context.device);
190
+ const server = context?.server && withoutIp(context.server);
191
+ return {
192
+ ...(context?.page && {
193
+ page: {
194
+ url: context.page.url,
195
+ host: context.page.host,
196
+ path: context.page.path,
197
+ title: context.page.title,
198
+ protocol: context.page.protocol,
199
+ referrer: context.page.referrer,
200
+ ...(context.page.search && { search: context.page.search }),
201
+ },
202
+ }),
203
+ ...(device && Object.keys(device).length > 0 && { device }),
204
+ ...(context?.utm && { utm: context.utm }),
205
+ ...(server && Object.keys(server).length > 0 && { server }),
206
+ };
207
+ }
208
+ /**
209
+ * Describe an SDK failure for logs. EmitKit errors add their error code,
210
+ * HTTP status, and request id, which tell auth, validation, and rate-limit
211
+ * failures apart; the message and response body are never logged.
212
+ */
213
+ describeError(error) {
214
+ const errorClass = this.getErrorClass(error);
215
+ try {
216
+ if (!(error instanceof Error) || error.name !== "EmitKitError") {
217
+ return errorClass;
218
+ }
219
+ const code = Reflect.get(error, "code");
220
+ const status = Reflect.get(error, "status");
221
+ const requestId = Reflect.get(error, "requestId");
222
+ return [
223
+ error.name,
224
+ typeof code === "string" ? code : undefined,
225
+ // The SDK reports status 0 when no response arrived.
226
+ typeof status === "number" && status > 0 ? status : undefined,
227
+ typeof requestId === "string" ? `request ${requestId}` : undefined,
228
+ ]
229
+ .filter((part) => part !== undefined)
230
+ .join(" ");
231
+ }
232
+ catch {
233
+ // Error description must never replace the original failure.
234
+ return errorClass;
235
+ }
236
+ }
237
+ /**
238
+ * Format event action into a human-readable title
239
+ * Converts: "user_signed_up" -> "User Signed Up"
240
+ */
241
+ formatEventTitle(action) {
242
+ return action
243
+ .split("_")
244
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
245
+ .join(" ");
246
+ }
247
+ /**
248
+ * Generate a description for the event
249
+ */
250
+ getEventDescription(event, context) {
251
+ // Use explicit description from properties if available
252
+ if (event.properties?.description &&
253
+ typeof event.properties.description === "string") {
254
+ return event.properties.description;
255
+ }
256
+ // Generate default description based on category
257
+ const categoryDescriptions = {
258
+ engagement: "User interaction event",
259
+ user: "User lifecycle event",
260
+ navigation: "Navigation event",
261
+ error: "Error or exception occurred",
262
+ performance: "Performance metric",
263
+ conversion: "Conversion event",
264
+ };
265
+ return categoryDescriptions[event.category] || undefined;
266
+ }
267
+ /**
268
+ * Get an appropriate icon for the event category
269
+ */
270
+ getEventIcon(category) {
271
+ const categoryIcons = {
272
+ engagement: "👆",
273
+ user: "👤",
274
+ navigation: "🧭",
275
+ error: "❌",
276
+ performance: "⚡",
277
+ conversion: "💰",
278
+ };
279
+ return categoryIcons[category];
280
+ }
281
+ /**
282
+ * Resolve the channel name for an event based on priority:
283
+ * 1. Event property __emitkit_channel (highest priority)
284
+ * 2. Category mapping via categoryChannelMap
285
+ * 3. Default channelName (fallback, default: 'general')
286
+ */
287
+ resolveChannelName(event, defaultChannel) {
288
+ // Priority 1: Check for explicit channel override in properties
289
+ if (event.properties?.__emitkit_channel &&
290
+ typeof event.properties.__emitkit_channel === "string") {
291
+ return event.properties.__emitkit_channel;
292
+ }
293
+ // Priority 2: Check category mapping
294
+ if (this.config.categoryChannelMap && event.category) {
295
+ const mappedChannel = this.config.categoryChannelMap[event.category];
296
+ if (mappedChannel) {
297
+ return mappedChannel;
298
+ }
299
+ }
300
+ // Priority 3: Use default channel
301
+ return defaultChannel || this.config.channelName || "general";
302
+ }
303
+ }
304
+ /**
305
+ * The SDK types metadata and properties as JSON values and serializes them
306
+ * with `JSON.stringify`, so trakoo's properties pass through unchanged.
307
+ */
308
+ function asJsonObject(value) {
309
+ return value;
310
+ }
311
+ function withoutIp(block) {
312
+ const { ip: _ip, ...rest } = block;
313
+ return rest;
314
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@trakoo/emitkit",
3
+ "version": "0.0.0",
4
+ "description": "EmitKit provider for trakoo",
5
+ "type": "module",
6
+ "exports": {
7
+ "./server": {
8
+ "types": "./dist/server.d.ts",
9
+ "import": "./dist/server.js",
10
+ "default": "./dist/server.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "sideEffects": false,
19
+ "keywords": [
20
+ "analytics",
21
+ "trakoo",
22
+ "emitkit",
23
+ "typescript"
24
+ ],
25
+ "author": "Chris Jayden <https://github.com/multiplehats>",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/multiplehats/trakoo.git",
30
+ "directory": "packages/emitkit"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/multiplehats/trakoo/issues"
34
+ },
35
+ "homepage": "https://trakoo.co/docs/providers/emitkit",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "engines": {
40
+ "node": ">=20"
41
+ },
42
+ "peerDependencies": {
43
+ "@emitkit/js": "^3.0.0-next.0",
44
+ "trakoo": "^1.2.1"
45
+ },
46
+ "devDependencies": {
47
+ "@emitkit/js": "^3.0.0-next.0",
48
+ "trakoo": "1.2.1"
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -p tsconfig.build.json",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "vitest run"
54
+ }
55
+ }