@ginger-ai/ginger-js 0.0.2 → 0.0.3

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 CHANGED
@@ -24,6 +24,12 @@ Install via NPM:
24
24
  npm install @ginger-ai/ginger-js
25
25
  ```
26
26
 
27
+ Install via Yarn:
28
+
29
+ ```bash
30
+ yarn add @ginger-ai/ginger-js
31
+ ```
32
+
27
33
  ## Implementation Guide
28
34
 
29
35
  ### NPM Implementation
@@ -33,7 +39,7 @@ import { GingerJsClient } from "@ginger-ai/ginger-js";
33
39
 
34
40
  async function initializeGinger() {
35
41
  const gingerjs = new GingerJsClient({
36
- apikey: "your-api-key"
42
+ apikey: "Public-Key"
37
43
  });
38
44
  const response = await gingerjs.initialize();
39
45
 
@@ -60,7 +66,7 @@ The library operates through a request session-based architecture:
60
66
  2. **Behavior Tracking** (Optional):
61
67
  - Requires: `event_type`, `request_id`, and `track_fields`
62
68
  - Monitors specified input elements for user interaction
63
- 3. **Data Submission**: Returns a structured◊◊ response:
69
+ 3. **Data Submission**: Returns a structured response:
64
70
  ```javascript
65
71
  {
66
72
  event_id: number,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ginger-ai/ginger-js",
3
3
  "type": "module",
4
- "version": "0.0.2",
4
+ "version": "0.0.3",
5
5
  "description": "GingerJs JavaScript agent for Single page application (SPA)",
6
6
  "main": "dist/ginger.cjs.js",
7
7
  "module": "dist/ginger.esm.js",
@@ -0,0 +1,106 @@
1
+ import { FieldMetrics } from "../core";
2
+
3
+ interface computeFieldParams {
4
+ id: string;
5
+ event: any;
6
+ currentField: any;
7
+ focusFields: any;
8
+ }
9
+
10
+ export const computeFieldInputMetrics = ({
11
+ id,
12
+ event,
13
+ currentField,
14
+ focusFields,
15
+ }: computeFieldParams) => {
16
+ const now = performance.now();
17
+ const lastKeyStrokeTime = currentField.ended_at;
18
+
19
+ const isCorrected = isCorrectionAction(event.inputType);
20
+ const isPaste = isPasteAction(event.inputType);
21
+ const isPause = isPauseAction(now, lastKeyStrokeTime);
22
+ const isAutoFill = isAutofillAction(event, currentField);
23
+ const focusTime = focusFields?.[id];
24
+
25
+ const started_at = currentField.started_at || now;
26
+ const paste_count = currentField.paste_count + (isPaste ? 1 : 0);
27
+ const autofill_count = currentField.autofill_count + (isAutoFill ? 1 : 0);
28
+ const pauses = currentField.pauses + (isPause ? 1 : 0);
29
+ const corrections_count =
30
+ currentField.corrections_count + (isCorrected ? 1 : 0);
31
+ const interaction_count = currentField.interaction_count + 1;
32
+ const characters_count = event.target.value.length;
33
+ const pause_durations = calculatePauseDurations(currentField, isPause, now);
34
+ const hesitation_time = calculateHesitationTime(started_at, focusTime);
35
+
36
+ return {
37
+ ...currentField,
38
+ field_name: id,
39
+ started_at,
40
+ ended_at: now,
41
+ interaction_count,
42
+ characters_count,
43
+ paste_count,
44
+ autofill_count,
45
+ corrections_count,
46
+ pauses,
47
+ hesitation_time,
48
+ pause_durations,
49
+ };
50
+ };
51
+
52
+ const isCorrectionAction = (inputType: string) =>
53
+ inputType === "deleteContentBackward" ||
54
+ inputType == "deleteContentForward" ||
55
+ inputType == "deleteSoftLineBackward";
56
+
57
+ const isPasteAction = (inputType: string) =>
58
+ inputType == "insertFromPaste" || inputType == "insertFromDrop";
59
+
60
+ const isPauseAction = (
61
+ now: number,
62
+ lastKeyStrokeTime: number,
63
+ waitTime: number = 2500
64
+ ) => {
65
+ const timeSinceLastKeystroke = now - lastKeyStrokeTime;
66
+ return lastKeyStrokeTime !== 0 && timeSinceLastKeystroke > waitTime;
67
+ };
68
+
69
+ const isAutofillAction = (event: any, currentField: any) => {
70
+ const { inputType, isComposing } = event;
71
+ const isPaste = isPasteAction(inputType);
72
+ const isRedo = isRedoAction(inputType);
73
+ const isUndo = isUndoAction(inputType);
74
+
75
+ if (isPaste || isRedo || isUndo || isComposing) return false;
76
+
77
+ const prevCharacterCount = currentField.characters_count;
78
+ const currentCharacterCount = event.target.value.length;
79
+ const diff = currentCharacterCount - prevCharacterCount;
80
+
81
+ return diff > 1;
82
+ };
83
+
84
+ const isRedoAction = (inputType: string) => inputType === "historyRedo";
85
+ const isUndoAction = (inputType: string) => inputType === "historyUndo";
86
+
87
+ const calculatePauseDurations = (
88
+ currentField: FieldMetrics,
89
+ isPause: boolean,
90
+ now: number
91
+ ) => {
92
+ if (!isPause) return currentField.pause_durations;
93
+
94
+ const duration = now - currentField.ended_at;
95
+ const pauseDurations = [...currentField.pause_durations, duration];
96
+ return pauseDurations;
97
+ };
98
+
99
+ const calculateHesitationTime = (
100
+ typeStartTime: number,
101
+ focusTime: number | null | undefined
102
+ ) => {
103
+ if (!focusTime) return 0;
104
+ const hesitationTime = typeStartTime - focusTime;
105
+ return Math.max(0, hesitationTime);
106
+ };
@@ -0,0 +1,60 @@
1
+ import { FieldMetrics } from "../core";
2
+
3
+ export function createFieldMetric(id: string, ltm: boolean): FieldMetrics {
4
+ return {
5
+ field_name: id,
6
+ ltm: ltm,
7
+ started_at: 0,
8
+ ended_at: 0,
9
+ interaction_count: 0,
10
+ characters_count: 0,
11
+ paste_count: 0,
12
+ autofill_count: 0,
13
+ corrections_count: 0,
14
+ modifier_key_count: 0,
15
+ pauses: 0,
16
+ hesitation_time: 0,
17
+ pause_durations: [],
18
+ key_hold_times: [],
19
+ };
20
+ }
21
+
22
+ export const MODIFIER_KEYS = [
23
+ "Meta",
24
+ "Alt",
25
+ "Control",
26
+ "Shift",
27
+ "CapsLock",
28
+ "Tab",
29
+ "Backspace",
30
+ "Enter",
31
+ "Escape",
32
+ "ArrowUp",
33
+ "ArrowDown",
34
+ "ArrowLeft",
35
+ "ArrowRight",
36
+ "Home",
37
+ "End",
38
+ "PageUp",
39
+ "PageDown",
40
+ "Insert",
41
+ "Delete",
42
+ "ContextMenu",
43
+ "NumLock",
44
+ "ScrollLock",
45
+ "Pause",
46
+ ];
47
+
48
+ export const computeFlightTimeMetric = (flights: any) => {
49
+ return flights.reduce((acc: any, curr: any, index: number, arr: any) => {
50
+ if (index === 0) return acc;
51
+
52
+ const prev = arr[index - 1];
53
+
54
+ if (curr.downTime != null && prev.upTime != null) {
55
+ const flightTime = curr.downTime - prev.upTime;
56
+ acc.push(flightTime);
57
+ }
58
+ return acc;
59
+ }, []);
60
+ };
@@ -1,77 +1,121 @@
1
- import { GingerClientError, FieldMetrics, FillEnum, FillMethod, pageVisibility, Field } from "../core";
2
-
3
- interface TrackedFieldMetrics extends FieldMetrics {
4
- readonly element: HTMLInputElement | HTMLTextAreaElement;
5
- }
6
-
7
- interface EventConfig {
8
- readonly pauseThresholdMs: number;
9
- }
1
+ import {
2
+ GingerClientError,
3
+ FieldMetrics,
4
+ pageVisibility,
5
+ Field,
6
+ FocusMetrics,
7
+ } from "../core";
8
+ import { computeFieldInputMetrics } from "./computeFieldInputMetrics";
9
+ import {
10
+ computeFlightTimeMetric,
11
+ createFieldMetric,
12
+ MODIFIER_KEYS,
13
+ } from "./helpers";
14
+
15
+ type KeyTiming = { downTime: number };
16
+ type ActiveField = Map<string, KeyTiming>;
17
+ type ActiveKeys = Map<string, ActiveField>;
18
+ type HoldTime = Record<string, number[]>;
19
+ type FullKeyTiming = {
20
+ upTime: number | null;
21
+ downTime: number | null;
22
+ key: string;
23
+ };
24
+ type FlightTime = Record<string, FullKeyTiming[]>;
25
+ type ModifierKey = Record<string, number>;
10
26
 
11
- const { getCount, addListener, removeListener } = pageVisibility();
27
+ const { getCount, addPageListener, removePageListener } = pageVisibility();
12
28
 
13
29
  class EventTracker {
14
- private static readonly DEFAULT_CONFIG: EventConfig = {
15
- pauseThresholdMs: 1500,
16
- };
30
+ private trackedFieldsMap: Record<string, FieldMetrics> = {};
31
+ private focusFieldsMap: FocusMetrics = {};
17
32
 
18
- private readonly config: EventConfig;
19
- private isInitialized = false;
20
- private readonly fields: TrackedFieldMetrics[] = [];
21
- private readonly trackedFieldIds = new Set<string>();
33
+ private activeKeyMap: ActiveKeys = new Map();
34
+ private holdTimeMap: HoldTime = {};
35
+ private flightTimeMap: FlightTime = {};
36
+ private modifierKeysMap: ModifierKey = {};
22
37
 
23
- constructor(config: Partial<EventConfig> = {}) {
24
- this.config = { ...EventTracker.DEFAULT_CONFIG, ...config };
25
- addListener();
38
+ constructor() {
39
+ addPageListener();
26
40
  }
27
41
 
28
42
  /**
29
- * Initialize tracking for multiple form fields
43
+ * Field that were previously tracked and needs to be removed
30
44
  */
31
- public initializeTracking(fields: readonly Field[]): void {
32
- const newFields = fields.filter(
33
- (field) => !this.trackedFieldIds.has(field.id)
34
- );
45
+ public removeUntrackedFieldsMap(fields: Field[]) {
46
+ const trackedIds = Object.keys(this.trackedFieldsMap);
47
+ const newIds = fields.map((field) => field.id);
48
+ const IdsToUntrack = trackedIds.filter((id) => !newIds.includes(id));
49
+
50
+ IdsToUntrack.forEach((id) => {
51
+ this.removeFromTrackedMap(id);
52
+ this.removeFromFocusMap(id);
53
+ this.removeFromHoldTimeMap(id);
54
+ this.removeFromFlightTimeMap(id);
55
+ this.removeFromModifierKeysMap(id);
56
+ });
57
+ }
35
58
 
36
- newFields.forEach((field) => this.trackField(field));
37
- this.isInitialized = true;
59
+ private removeFromTrackedMap(id: string) {
60
+ const map = this.trackedFieldsMap;
61
+ const field = map[id];
62
+ if (!field) return;
63
+ delete map[id];
38
64
  }
39
65
 
40
- /**
41
- * Get all tracked metrics data (excluding DOM elements)
42
- */
43
- get metrics(): FieldMetrics[] {
44
- this.ensureInitialized();
45
- return this.fields.map(({ element, ...field }) => field);
66
+ private removeFromFocusMap(id: string) {
67
+ const map = this.focusFieldsMap;
68
+ const field = map[id];
69
+ if (!field) return;
70
+ delete map[id];
71
+ }
72
+
73
+ private removeFromHoldTimeMap(id: string) {
74
+ const map = this.holdTimeMap;
75
+ const field = map[id];
76
+ if (!field) return;
77
+ delete map[id];
78
+ }
79
+
80
+ private removeFromFlightTimeMap(id: string) {
81
+ const map = this.flightTimeMap;
82
+ const field = map[id];
83
+ if (!field) return;
84
+ delete map[id];
85
+ }
86
+
87
+ private removeFromModifierKeysMap(id: string) {
88
+ const map = this.modifierKeysMap;
89
+ const field = map[id];
90
+ if (!field) return;
91
+ delete map[id];
46
92
  }
47
93
 
48
94
  /**
49
- * Remove tracking from specific field or all fields
95
+ * Initialize tracking for multiple form fields
50
96
  */
51
- public removeTracking(fieldId?: string): void {
52
- if (fieldId) {
53
- this.removeFieldTracking(fieldId);
54
- } else {
55
- this.removeAllTracking();
56
- }
97
+ public startTracking(fields: readonly Field[]): void {
98
+ fields.forEach((field) => this.trackField(field));
57
99
  }
58
100
 
59
101
  // Private implementation methods
60
102
 
61
103
  private trackField(field: Field): void {
62
104
  const { id, ltm } = field;
63
- const element = this.getValidElement(id);
64
- const fieldMetric = this.createFieldMetric(id, element, ltm);
105
+ const element = this.validateElement(id);
65
106
 
66
- this.fields.push(fieldMetric);
67
- this.trackedFieldIds.add(id);
107
+ const metrics = createFieldMetric(id, ltm ?? false);
108
+ this.trackedFieldsMap[id] = metrics;
68
109
 
69
- // Use bound method to maintain context
70
- const boundHandler = this.createInputHandler(fieldMetric);
71
- element.addEventListener("input", boundHandler);
110
+ element.addEventListener("input", (e) => this.computeChangeMetrics(id, e));
111
+ element.addEventListener("focus", (e) => this.computeFocusMetrics(id, e));
112
+ element.addEventListener("keydown", (e) =>
113
+ this.computeKeydownMetrics(id, e)
114
+ );
115
+ element.addEventListener("keyup", (e) => this.computeKeyupMetrics(id, e));
72
116
  }
73
117
 
74
- private getValidElement(id: string): HTMLInputElement | HTMLTextAreaElement {
118
+ private validateElement(id: string): HTMLInputElement | HTMLTextAreaElement {
75
119
  const element = document.getElementById(id);
76
120
 
77
121
  if (!element) {
@@ -80,7 +124,7 @@ class EventTracker {
80
124
  );
81
125
  }
82
126
 
83
- if (!this.isValidInputElement(element)) {
127
+ if (!this.validateType(element)) {
84
128
  throw new GingerClientError(
85
129
  `Element with ID "${id}" must be an HTMLInputElement or HTMLTextAreaElement.`
86
130
  );
@@ -89,7 +133,7 @@ class EventTracker {
89
133
  return element;
90
134
  }
91
135
 
92
- private isValidInputElement(
136
+ private validateType(
93
137
  element: Element
94
138
  ): element is HTMLInputElement | HTMLTextAreaElement {
95
139
  return (
@@ -98,159 +142,136 @@ class EventTracker {
98
142
  );
99
143
  }
100
144
 
101
- private createFieldMetric(
102
- id: string,
103
- element: HTMLInputElement | HTMLTextAreaElement,
104
- ltm?: boolean
105
- ): TrackedFieldMetrics {
106
- return {
107
- field_name: id,
108
- started_at: 0,
109
- ended_at: 0,
110
- interaction_count: 0,
111
- fill_method: null,
112
- paste_count: 0,
113
- ltm: ltm ?? false,
114
- corrections_count: 0,
115
- pauses: 0,
116
- pauseDurations: [],
117
- element,
118
- };
119
- }
145
+ private computeChangeMetrics(id: string, event: Event): void {
146
+ const map = this.trackedFieldsMap;
147
+ const currentField = map[id];
148
+ if (!currentField) return;
120
149
 
121
- private createInputHandler(field: TrackedFieldMetrics) {
122
- return (event: Event): void => {
123
- this.handleInput(field, event as InputEvent);
124
- };
150
+ const focusFieldsMap = this.focusFieldsMap;
151
+
152
+ const computedMetrics = computeFieldInputMetrics({
153
+ id,
154
+ event,
155
+ currentField,
156
+ focusFields: focusFieldsMap,
157
+ });
158
+
159
+ map[id] = computedMetrics;
125
160
  }
126
161
 
127
- private handleInput(
128
- currentField: TrackedFieldMetrics,
129
- event: InputEvent
130
- ): void {
131
- const now = performance.now();
132
- const lastInteractionTime = currentField.ended_at || 0;
162
+ private computeFocusMetrics(id: string, event: Event) {
163
+ const map = this.focusFieldsMap;
133
164
 
134
- const interactions = this.analyzeInteraction(
135
- currentField,
136
- event,
137
- now,
138
- lastInteractionTime
139
- );
165
+ if (map && !map?.[id]) {
166
+ const now = performance.now();
167
+ map[id] = now;
168
+ }
169
+ }
140
170
 
141
- this.updateFieldMetrics(currentField, interactions, now);
171
+ private computeKeydownMetrics(id: string, event: any) {
172
+ const { key, timeStamp } = event;
173
+ this.addToActiveKeyStore(id, key, timeStamp);
174
+ this.addToFlightKeyStore(id, key, timeStamp);
175
+ this.computeModifierKeysCount(id, event);
142
176
  }
143
177
 
144
- private analyzeInteraction(
145
- field: TrackedFieldMetrics,
146
- event: InputEvent,
147
- now: number,
148
- lastInteractionTime: number
149
- ) {
150
- const fillMethod = this.determineFillMethod(field, event);
151
-
152
- return {
153
- fillMethod,
154
- isCorrection: this.isCorrection(event),
155
- isPaste: this.isPaste(fillMethod, event),
156
- isPause: this.isPause(now, lastInteractionTime),
157
- pauseDuration: now - lastInteractionTime,
158
- };
178
+ private addToActiveKeyStore(id: string, key: string, timestamp: number) {
179
+ const map = this.activeKeyMap;
180
+ const field = map.get(id) ?? new Map<string, KeyTiming>();
181
+ const downTime = timestamp;
182
+
183
+ if (field.has(key)) return;
184
+
185
+ field.set(key, { downTime });
186
+ map.set(id, field);
159
187
  }
160
188
 
161
- private updateFieldMetrics(
162
- field: TrackedFieldMetrics,
163
- interactions: ReturnType<typeof this.analyzeInteraction>,
164
- now: number
165
- ): void {
166
- // Initialize start time on first interaction
167
- if (!field.started_at) {
168
- field.started_at = now;
169
- }
189
+ private addToFlightKeyStore(id: string, key: string, timestamp: number) {
190
+ const map = this.flightTimeMap;
191
+ const field = map[id] ?? [];
170
192
 
171
- field.ended_at = now;
172
- field.fill_method = interactions.fillMethod;
173
- field.interaction_count += 1;
193
+ const keyTime = { key, downTime: timestamp, upTime: null };
194
+ field.push(keyTime);
174
195
 
175
- if (interactions.isCorrection) field.corrections_count += 1;
176
- if (interactions.isPaste) field.paste_count += 1;
177
- if (interactions.isPause) {
178
- field.pauses += 1;
179
- field.pauseDurations.push(interactions.pauseDuration);
180
- }
196
+ map[id] = field;
181
197
  }
182
198
 
183
- private determineFillMethod(
184
- field: TrackedFieldMetrics,
185
- event: InputEvent
186
- ): FillMethod {
187
- const inputType = event.inputType;
188
- if (!inputType) return FillEnum.paste;
189
-
190
- const currentMethod: FillMethod =
191
- inputType === "insertText"
192
- ? FillEnum.typed
193
- : inputType === "insertFromPaste"
194
- ? FillEnum.paste
195
- : FillEnum.mixed;
196
-
197
- // If methods have been mixed, maintain "mixed" state
198
- if (field.fill_method && field.fill_method !== currentMethod) {
199
- return FillEnum.mixed;
199
+ private computeModifierKeysCount = (id: string, e: any) => {
200
+ const key = e.key;
201
+ const modifierKeys = this.modifierKeysMap;
202
+
203
+ if (MODIFIER_KEYS.includes(key)) {
204
+ modifierKeys[id] = (modifierKeys[id] || 0) + 1;
200
205
  }
206
+ };
201
207
 
202
- return currentMethod;
203
- }
208
+ private computeKeyupMetrics(id: string, e: any) {
209
+ const { key, timeStamp } = e;
204
210
 
205
- private isCorrection(event: InputEvent): boolean {
206
- return event.inputType === "deleteContentBackward";
211
+ this.computeKeyHoldTime(id, key, timeStamp);
212
+ this.computeFlightTime(id, key, timeStamp);
207
213
  }
208
214
 
209
- private isPaste(fillMethod: FillMethod, event: InputEvent): boolean {
210
- return (
211
- fillMethod === FillEnum.paste || event.inputType === "insertFromPaste"
212
- );
213
- }
215
+ private computeKeyHoldTime = (id: string, key: string, timestamp: number) => {
216
+ const map = this.activeKeyMap;
214
217
 
215
- private isPause(now: number, lastInteractionTime: number): boolean {
216
- if (lastInteractionTime === 0) return false;
218
+ const field = map.get(id);
219
+ if (!field) return;
217
220
 
218
- const timeSinceLastKeystroke = now - lastInteractionTime;
219
- return timeSinceLastKeystroke > this.config.pauseThresholdMs;
220
- }
221
+ const fieldObject = field.get(key);
222
+ if (!fieldObject) return;
221
223
 
222
- private removeFieldTracking(fieldId: string): void {
223
- const index = this.fields.findIndex(
224
- (field) => field.field_name === fieldId
225
- );
224
+ const { downTime } = fieldObject;
225
+ const upTime = timestamp;
226
+ const holdTime = upTime - downTime;
226
227
 
227
- if (index === -1) return;
228
+ const holdTimes = this.holdTimeMap;
229
+ const holdTimeArray = (holdTimes[id] ||= []);
228
230
 
229
- const field = this.fields[index];
230
- this.cleanupFieldTracking(field);
231
+ holdTimeArray.push(holdTime);
231
232
 
232
- this.fields.splice(index, 1);
233
- this.trackedFieldIds.delete(fieldId);
234
- }
233
+ field.delete(key);
234
+ };
235
+
236
+ private computeFlightTime = (id: string, key: string, timestamp: number) => {
237
+ const map = this.flightTimeMap;
238
+ const fieldArray = map[id];
239
+ if (!fieldArray) return;
235
240
 
236
- private removeAllTracking(): void {
237
- this.fields.forEach((field) => this.cleanupFieldTracking(field));
238
- this.fields.length = 0;
239
- this.trackedFieldIds.clear();
241
+ const lastEntry = fieldArray.findLast((f) => f.key === key);
242
+ if (!lastEntry) return;
243
+
244
+ lastEntry.upTime = timestamp;
245
+
246
+ const index = fieldArray.lastIndexOf(lastEntry);
247
+ if (index !== -1) {
248
+ fieldArray[index] = lastEntry;
249
+ }
250
+ };
251
+
252
+ private getFlightTime(id: string) {
253
+ const flights = this.flightTimeMap[id] || [];
254
+ return computeFlightTimeMetric(flights);
240
255
  }
241
256
 
242
- private cleanupFieldTracking(field: TrackedFieldMetrics): void {
243
- // Note: We can't remove the exact handler since we're using bound methods
244
- // In a real implementation, you'd want to store the bound handlers
245
- field.element.removeEventListener("input", this.createInputHandler(field));
257
+ public reset() {
258
+ this.trackedFieldsMap = {};
259
+ this.focusFieldsMap = {};
260
+ this.activeKeyMap = new Map();
261
+ this.holdTimeMap = {};
262
+ this.flightTimeMap = {};
263
+ this.modifierKeysMap = {};
246
264
  }
247
265
 
248
- private ensureInitialized(): void {
249
- if (!this.isInitialized) {
250
- throw new GingerClientError(
251
- "Ginger.trackEvent must be initialized before data can be fetched."
252
- );
253
- }
266
+ public serialize() {
267
+ const metrics = this.trackedFieldsMap;
268
+ return Object.values(metrics).map((metric) => {
269
+ const id = metric.field_name;
270
+ const key_hold_times = this.holdTimeMap[id] || [];
271
+ const flight_time = this.getFlightTime(id);
272
+ const modifier_key_count = this.modifierKeysMap[id];
273
+ return { ...metric, key_hold_times, flight_time, modifier_key_count };
274
+ });
254
275
  }
255
276
  }
256
277
 
@@ -258,22 +279,21 @@ class EventTracker {
258
279
  const eventTracker = new EventTracker();
259
280
 
260
281
  // Public API exports
261
- export const trackInputs = (fields: readonly Field[]): void => {
262
- eventTracker.initializeTracking(fields);
282
+ const trackInputs = (fields: Field[]): void => {
283
+ eventTracker.removeUntrackedFieldsMap(fields);
284
+ eventTracker.startTracking(fields);
263
285
  };
264
286
 
265
- export const getTrackedFields = (): {
287
+ const trackingData = (): {
266
288
  distractions_count: number;
267
289
  fields: FieldMetrics[];
268
290
  } => {
269
291
  return {
270
292
  distractions_count: getCount(),
271
- fields: eventTracker.metrics,
293
+ fields: eventTracker.serialize(),
272
294
  };
273
295
  };
274
296
 
275
- export const removeTracking = (fieldId?: string): void => {
276
- eventTracker.removeTracking(fieldId);
277
- };
297
+ const reset = () => eventTracker.reset();
278
298
 
279
- export { EventTracker, removeListener };
299
+ export { EventTracker, removePageListener, trackInputs, trackingData, reset };
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  trackInputs,
3
- getTrackedFields,
4
- removeListener,
3
+ trackingData,
4
+ removePageListener,
5
+ reset,
5
6
  } from "../behaviour";
6
7
 
7
8
  import { buildInitialPayload } from "../device";
@@ -23,10 +24,14 @@ import {
23
24
  BehaviourPayloadResponseData,
24
25
  } from "../core";
25
26
 
27
+ type EventPayload = {
28
+ event: BehaviourPayload;
29
+ };
30
+
26
31
  interface GingerClient {
27
32
  initialize(configs: Configurations): Promise<PayloadResponse | undefined>;
28
33
  trackEvent(params: BehaviourParams): void;
29
- getTrackedData(): BehaviourPayload;
34
+ getTrackedData(): EventPayload;
30
35
  submitEvent(): Promise<BehaviourPayloadResponseData>;
31
36
  }
32
37
 
@@ -43,7 +48,7 @@ export class GingerJsClient implements GingerClient {
43
48
 
44
49
  async initialize() {
45
50
  try {
46
- const sdkInfo = { name: '@ginger-ai/ginger-js', version: '0.0.2' };
51
+ const sdkInfo = { name: "@ginger-ai/ginger-js", version: "0.0.3" };
47
52
  const payload = await buildInitialPayload(this.requestId, sdkInfo);
48
53
  const response = await this.httpClient.post<Payload, PayloadResponse>({
49
54
  url: `/api/v1/devices`,
@@ -99,13 +104,15 @@ export class GingerJsClient implements GingerClient {
99
104
  );
100
105
  }
101
106
 
102
- const trackData = getTrackedFields();
107
+ const trackData = trackingData();
103
108
 
104
109
  return {
105
- event_type: this.trackDetails.event_type,
106
- request_id: this.trackDetails.request_id,
107
- fingerprint_id: this.trackDetails.fingerprint_id,
108
- data: { ...trackData },
110
+ event: {
111
+ event_type: this.trackDetails.event_type,
112
+ request_id: this.trackDetails.request_id,
113
+ fingerprint_id: this.trackDetails.fingerprint_id,
114
+ data: { ...trackData },
115
+ },
109
116
  };
110
117
  };
111
118
 
@@ -117,11 +124,12 @@ export class GingerJsClient implements GingerClient {
117
124
 
118
125
  try {
119
126
  const response = await this.httpClient.post<
120
- BehaviourPayload,
127
+ EventPayload,
121
128
  BehaviourPayloadResponse
122
129
  >({ url: `/api/v1/events`, payload });
123
130
 
124
- removeListener();
131
+ removePageListener();
132
+ reset();
125
133
  return response.data;
126
134
  } catch (error) {
127
135
  throw new GingerClientError(
@@ -1,4 +1,4 @@
1
1
  export const CONSTANTS = {
2
2
  LIVE_URL: "https://app.getrayyan.com",
3
- TEST_URL: "https://sandbox.useginger.ai"
3
+ TEST_URL: "https://api-sandbox.useginger.ai"
4
4
  }
@@ -12,9 +12,15 @@ export interface FieldMetrics {
12
12
  started_at: number;
13
13
  ended_at: number;
14
14
  interaction_count: number;
15
- fill_method: FillMethod;
15
+ characters_count: number;
16
16
  paste_count: number;
17
+ autofill_count: number;
17
18
  corrections_count: number;
18
19
  pauses: number;
19
- pauseDurations: number[];
20
+ hesitation_time: number;
21
+ pause_durations: number[];
22
+ key_hold_times: number[];
23
+ modifier_key_count: number;
20
24
  }
25
+
26
+ export type FocusMetrics = Record<string, number | null>
@@ -4,7 +4,7 @@ import { GingerClientError } from "./util/error";
4
4
  export default function getBasePath(apikey: string) {
5
5
  if (apikey.includes("sk_live") || apikey.includes("pk_live"))
6
6
  return CONSTANTS.LIVE_URL;
7
-
7
+
8
8
  return CONSTANTS.TEST_URL;
9
9
  }
10
10
 
@@ -21,9 +21,9 @@ export const pageVisibility = () => {
21
21
 
22
22
  return {
23
23
  getCount: () => leaveCount,
24
- addListener: () =>
24
+ addPageListener: () =>
25
25
  document.addEventListener("visibilitychange", visibilityHandler),
26
- removeListener: () =>
26
+ removePageListener: () =>
27
27
  document.removeEventListener("visibilitychange", visibilityHandler),
28
28
  };
29
29
  };
@@ -31,3 +31,43 @@ export const pageVisibility = () => {
31
31
  export const validatePresence = (key: string) => {
32
32
  if (!key) throw new GingerClientError("'apikey' must be provided.");
33
33
  };
34
+
35
+ const getEncoderKey = (apikey: string): string => {
36
+ const last6 = apikey.slice(-6);
37
+ const secondCharacterAfterPrefix = apikey.split("_")[2]?.[1];
38
+ return last6 + secondCharacterAfterPrefix;
39
+ };
40
+
41
+ export const encode = (apikey: string, payload: unknown): string => {
42
+ try {
43
+ const key = getEncoderKey(apikey);
44
+ const data = JSON.stringify(payload);
45
+
46
+ let encoded = "";
47
+
48
+ for (let i = 0; i < data.length; i++) {
49
+ const keyChar = key.charCodeAt(i % key.length);
50
+ // eslint-disable-next-line no-bitwise
51
+ encoded += String.fromCharCode(data.charCodeAt(i) ^ keyChar);
52
+ }
53
+
54
+ return btoa(encoded);
55
+ } catch (error) {
56
+ throw new Error("Encoding failed");
57
+ }
58
+ };
59
+
60
+ export const decode = (apikey: string, encoded: string): unknown => {
61
+ const key = getEncoderKey(apikey);
62
+ const decoded = atob(encoded);
63
+
64
+ let result = "";
65
+
66
+ for (let i = 0; i < decoded.length; i++) {
67
+ const keyChar = key.charCodeAt(i % key.length);
68
+ // eslint-disable-next-line no-bitwise
69
+ result += String.fromCharCode(decoded.charCodeAt(i) ^ keyChar);
70
+ }
71
+
72
+ return JSON.parse(result);
73
+ };
@@ -1,3 +1,4 @@
1
+ import { encode } from "../helpers";
1
2
  import { handleException } from "../util/error";
2
3
 
3
4
  type Method = "POST" | "GET" | "PUT" | "DELETE";
@@ -8,7 +9,6 @@ export const makeRequest = async <Request, Response>(
8
9
  method: Method,
9
10
  apikey: string
10
11
  ): Promise<Response> => {
11
-
12
12
  try {
13
13
  const response = await fetch(url, {
14
14
  method,
@@ -17,7 +17,7 @@ export const makeRequest = async <Request, Response>(
17
17
  Accept: "application/json",
18
18
  Authorization: `Bearer ${apikey}`,
19
19
  },
20
- body: JSON.stringify(payload),
20
+ body: JSON.stringify(encode(apikey, payload)),
21
21
  });
22
22
 
23
23
  if (!response.ok) {