@vtex/faststore-plugin-buyer-portal 1.3.27 → 1.3.28

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +8 -2
  2. package/package.json +1 -1
  3. package/src/features/addresses/components/AddRecipientsDrawer/AddRecipientsDrawer.tsx +20 -2
  4. package/src/features/addresses/components/CreateAddressDrawer/CreateAddressDrawer.tsx +50 -4
  5. package/src/features/addresses/components/DeleteAddressDrawer/DeleteAddressDrawer.tsx +26 -2
  6. package/src/features/addresses/components/DeleteAddressLocationDrawer/DeleteAddressLocationDrawer.tsx +29 -3
  7. package/src/features/addresses/components/DeleteRecipientAddressDrawer/DeleteRecipientAddressDrawer.tsx +24 -2
  8. package/src/features/addresses/components/EditAddressDrawer/EditAddressDrawer.tsx +28 -2
  9. package/src/features/addresses/components/EditAddressLocationDrawer/EditAddressLocationDrawer.tsx +30 -4
  10. package/src/features/addresses/components/EditRecipientAddressDrawer/EditRecipientAddressDrawer.tsx +30 -2
  11. package/src/features/addresses/components/LocationsDrawer/LocationsDrawer.tsx +29 -2
  12. package/src/features/budgets/components/BudgetDeleteDrawer/BudgetDeleteDrawer.tsx +32 -4
  13. package/src/features/budgets/components/CreateBudgetDrawer/CreateBudgetDrawer.tsx +59 -4
  14. package/src/features/budgets/components/EditBudgetDrawer/EditBudgetDrawer.tsx +28 -1
  15. package/src/features/buying-policies/components/AddBuyingPolicyDrawer/AddBuyingPolicyDrawer.tsx +48 -5
  16. package/src/features/buying-policies/components/DeleteBuyingPolicyDrawer/DeleteBuyingPolicyDrawer.tsx +28 -2
  17. package/src/features/buying-policies/components/UpdateBuyingPolicyDrawer/UpdateBuyingPolicyDrawer.tsx +49 -5
  18. package/src/features/custom-fields/components/CreateCustomFieldValueDrawer/CreateCustomFieldValueDrawer.tsx +60 -5
  19. package/src/features/custom-fields/components/DeleteCustomFieldValueDrawer/DeleteCustomFieldValueDrawer.tsx +61 -5
  20. package/src/features/custom-fields/components/UpdateCustomFieldValueDrawer/UpdateCustomFieldValueDrawer.tsx +32 -3
  21. package/src/features/org-units/components/CreateOrgUnitDrawer/CreateOrgUnitDrawer.tsx +18 -0
  22. package/src/features/org-units/components/DeleteOrgUnitDrawer/DeleteOrgUnitDrawer.tsx +28 -0
  23. package/src/features/org-units/components/UpdateOrgUnitDrawer/UpdateOrgUnitDrawer.tsx +28 -0
  24. package/src/features/shared/hooks/analytics/types.ts +14 -0
  25. package/src/features/shared/hooks/analytics/useAnalytics.ts +249 -0
  26. package/src/features/shared/hooks/index.ts +1 -0
  27. package/src/features/shared/services/logger/analytics/analytics.ts +101 -0
  28. package/src/features/shared/services/logger/analytics/constants.ts +83 -0
  29. package/src/features/shared/services/logger/analytics/types.ts +108 -0
  30. package/src/features/shared/services/logger/index.ts +1 -0
  31. package/src/features/shared/utils/constants.ts +1 -1
  32. package/src/features/users/components/CreateUserDrawer/CreateUserDrawer.tsx +24 -0
  33. package/src/features/users/components/DeleteUserDrawer/DeleteUserDrawer.tsx +23 -2
  34. package/src/features/users/components/UpdateUserDrawer/UpdateUserDrawer.tsx +45 -4
@@ -1,8 +1,9 @@
1
1
  import { useUI } from "@faststore/ui";
2
2
 
3
3
  import { UpdateEntityDrawer } from "../../../shared/components/CustomField/update-custom-field/UpdateCustomFieldDrawer";
4
- import { useBuyerPortal } from "../../../shared/hooks";
4
+ import { useAnalytics, useBuyerPortal } from "../../../shared/hooks";
5
5
  import { useUpdateCustomFieldValue } from "../../../shared/hooks/custom-field";
6
+ import { ANALYTICS_EVENTS } from "../../../shared/services/logger/analytics/constants";
6
7
  import { CustomFieldData } from "../../types";
7
8
 
8
9
  import type { BasicDrawerProps } from "../../../shared/components";
@@ -29,12 +30,27 @@ export function UpdateCustomFieldValueDrawer({
29
30
  const {
30
31
  clientContext: { cookie },
31
32
  } = useBuyerPortal();
33
+ const { trackEntityEdited, trackEntityEditError } = useAnalytics({
34
+ entityType: "custom_field",
35
+ entityId: currentData?.id,
36
+ defaultTimerName: "custom_field_edit",
37
+ shouldTrackDefaultTimer: !!currentData,
38
+ });
39
+
32
40
  const {
33
41
  mutate: updateCustomFieldValue,
34
42
  isLoading: isLoadignUpdateCustomFieldValue,
35
43
  } = useUpdateCustomFieldValue({
36
44
  options: {
37
45
  onSuccess: () => {
46
+ trackEntityEdited(ANALYTICS_EVENTS.CUSTOM_FIELD_EDITED, {
47
+ custom_field_type: customField,
48
+ custom_field_id: currentData?.id,
49
+ custom_field_old_name: currentData?.value,
50
+ org_unit_id: unitId,
51
+ contract_id: contractId,
52
+ });
53
+
38
54
  pushToast({
39
55
  message: `${customField} renamed successfully`,
40
56
  status: "INFO",
@@ -43,11 +59,24 @@ export function UpdateCustomFieldValueDrawer({
43
59
  close();
44
60
  refetch();
45
61
  },
46
- onError: (error) =>
62
+ onError: (error) => {
63
+ trackEntityEditError(
64
+ ANALYTICS_EVENTS.CUSTOM_FIELD_EDIT_ERROR,
65
+ "custom_field",
66
+ currentData?.id ?? "",
67
+ error,
68
+ {
69
+ custom_field_type: customField,
70
+ org_unit_id: unitId,
71
+ contract_id: contractId,
72
+ }
73
+ );
74
+
47
75
  pushToast({
48
76
  message: error.message,
49
77
  status: "ERROR",
50
- }),
78
+ });
79
+ },
51
80
  },
52
81
  });
53
82
 
@@ -12,6 +12,8 @@ import {
12
12
  InputText,
13
13
  Icon,
14
14
  } from "../../../shared/components";
15
+ import { useAnalytics } from "../../../shared/hooks";
16
+ import { ANALYTICS_EVENTS } from "../../../shared/services/logger/analytics/constants";
15
17
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
16
18
  import { useCreateNewOrgUnit } from "../../hooks";
17
19
 
@@ -34,6 +36,11 @@ export const CreateOrgUnitDrawer = ({
34
36
  }: CreateOrgUnitDrawerProps) => {
35
37
  const { pushToast } = useUI();
36
38
  const router = useRouter();
39
+ const { trackEntityCreated, trackEntityCreateError } = useAnalytics({
40
+ entityType: "org_unit",
41
+ defaultTimerName: "org_unit_creation",
42
+ shouldTrackDefaultTimer: true,
43
+ });
37
44
 
38
45
  const [parentOrgUnit, setParentOrgUnit] = useState(
39
46
  initialOrgUnit ?? options[0]
@@ -45,6 +52,12 @@ export const CreateOrgUnitDrawer = ({
45
52
  const handleCreateNewOrgUnitSuccess = (
46
53
  data: AwaitedType<typeof createNewOrgUnitService>
47
54
  ) => {
55
+ trackEntityCreated(ANALYTICS_EVENTS.ORG_UNIT_CREATED, "org_unit", {
56
+ org_unit_name: name,
57
+ parent_org_unit_id: parentOrgUnit?.id,
58
+ parent_org_unit_name: parentOrgUnit?.name,
59
+ });
60
+
48
61
  pushToast({
49
62
  message: "Organizational unit added successfully",
50
63
  status: "INFO",
@@ -77,6 +90,11 @@ export const CreateOrgUnitDrawer = ({
77
90
  description: string;
78
91
  };
79
92
 
93
+ trackEntityCreateError(ANALYTICS_EVENTS.ORG_UNIT_CREATE_ERROR, err, {
94
+ parent_org_unit_id: parentOrgUnit?.id,
95
+ error_type: error.code || "unknown",
96
+ });
97
+
80
98
  if (error.code === "InvalidOrganizationUnitName") {
81
99
  pushToast({
82
100
  message: "An organizational unit with the same name already exists",
@@ -11,6 +11,8 @@ import {
11
11
  InputText,
12
12
  type BasicDrawerProps,
13
13
  } from "../../../shared/components";
14
+ import { useAnalytics } from "../../../shared/hooks";
15
+ import { ANALYTICS_EVENTS } from "../../../shared/services/logger/analytics/constants";
14
16
  import { useGetUserByOrgUnitId } from "../../../users/hooks/useGetUserByOrgUnitId";
15
17
  import { useChildrenOrgUnits } from "../../hooks";
16
18
  import { useDeleteOrgUnit } from "../../hooks/useDeleteOrgUnit";
@@ -31,6 +33,12 @@ export const DeleteOrgUnitDrawer = ({
31
33
  const { childrenOrgUnits } = useChildrenOrgUnits(id, name);
32
34
  const { users } = useGetUserByOrgUnitId(id);
33
35
  const { pushToast } = useUI();
36
+ const { trackEvent } = useAnalytics({
37
+ entityType: "org_unit",
38
+ entityId: id,
39
+ defaultTimerName: "org_unit_delete",
40
+ shouldTrackDefaultTimer: true,
41
+ });
34
42
 
35
43
  const [confirmName, setConfirmName] = useState("");
36
44
  const [isTouched, setIsTouched] = useState(false);
@@ -39,6 +47,13 @@ export const DeleteOrgUnitDrawer = ({
39
47
  Boolean(childrenOrgUnits?.nodes?.length) || Boolean(users?.length);
40
48
 
41
49
  const handleDeleteOrgUnitSuccess = () => {
50
+ trackEvent(ANALYTICS_EVENTS.ORG_UNIT_DELETED, {
51
+ org_unit_id: id,
52
+ org_unit_name: name,
53
+ entity_type: "org_unit",
54
+ had_children: hasChildren,
55
+ });
56
+
42
57
  pushToast({
43
58
  message: "Organizational unit deleted successfully",
44
59
  status: "INFO",
@@ -55,6 +70,19 @@ export const DeleteOrgUnitDrawer = ({
55
70
 
56
71
  const { deleteOrgUnit, isDeleteOrgUnitLoading } = useDeleteOrgUnit({
57
72
  onSuccess: handleDeleteOrgUnitSuccess,
73
+ onError: (error) => {
74
+ trackEvent(ANALYTICS_EVENTS.ORG_UNIT_DELETE_ERROR, {
75
+ entity_id: id,
76
+ org_unit_name: name,
77
+ error_message: error instanceof Error ? error.message : String(error),
78
+ had_children: hasChildren,
79
+ });
80
+
81
+ pushToast({
82
+ message: "An error occurred while deleting the organizational unit",
83
+ status: "ERROR",
84
+ });
85
+ },
58
86
  });
59
87
 
60
88
  const handleDelete = () => {
@@ -10,6 +10,8 @@ import {
10
10
  ErrorMessage,
11
11
  InputText,
12
12
  } from "../../../shared/components";
13
+ import { useAnalytics } from "../../../shared/hooks";
14
+ import { ANALYTICS_EVENTS } from "../../../shared/services/logger/analytics/constants";
13
15
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
14
16
  import { useUpdateOrgUnit } from "../../hooks";
15
17
 
@@ -30,11 +32,23 @@ export const UpdateOrgUnitDrawer = ({
30
32
  }: UpdateOrgUnitDrawerProps) => {
31
33
  const router = useRouter();
32
34
  const { pushToast } = useUI();
35
+ const { trackEntityEdited, trackEntityEditError } = useAnalytics({
36
+ entityType: "org_unit",
37
+ entityId: id,
38
+ defaultTimerName: "org_unit_edit",
39
+ shouldTrackDefaultTimer: true,
40
+ });
33
41
  const [name, setName] = useState(initialName);
34
42
 
35
43
  const [isTouched, setIsTouched] = useState(false);
36
44
 
37
45
  const handleSuccess = () => {
46
+ trackEntityEdited(ANALYTICS_EVENTS.ORG_UNIT_EDITED, {
47
+ org_unit_id: id,
48
+ org_unit_old_name: initialName,
49
+ org_unit_new_name: name,
50
+ });
51
+
38
52
  pushToast({
39
53
  message: "Organizational unit successfully edited",
40
54
  status: "INFO",
@@ -64,6 +78,20 @@ export const UpdateOrgUnitDrawer = ({
64
78
 
65
79
  const { updateOrgUnit, isUpdateOrgUnitLoading } = useUpdateOrgUnit({
66
80
  onSuccess: handleSuccess,
81
+ onError: (error) => {
82
+ trackEntityEditError(
83
+ ANALYTICS_EVENTS.ORG_UNIT_EDIT_ERROR,
84
+ "org_unit",
85
+ id,
86
+ error,
87
+ {}
88
+ );
89
+
90
+ pushToast({
91
+ message: "An error occurred while editing the organizational unit",
92
+ status: "ERROR",
93
+ });
94
+ },
67
95
  });
68
96
 
69
97
  const handleConfirmClick = () => {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Timer tracking for multi-step flows
3
+ */
4
+ export interface AnalyticsTimer {
5
+ startTime: number;
6
+ metadata?: Record<string, unknown>;
7
+ }
8
+
9
+ export interface UseAnalyticsConfig {
10
+ entityType: string;
11
+ defaultTimerName: string;
12
+ entityId?: string;
13
+ shouldTrackDefaultTimer?: boolean;
14
+ }
@@ -0,0 +1,249 @@
1
+ import { useCallback, useEffect, useRef } from "react";
2
+
3
+ import { useDataIngestionApi } from "../../services/logger/analytics/analytics";
4
+
5
+ import type { AnalyticsTimer, UseAnalyticsConfig } from "./types";
6
+
7
+ export function useAnalytics(options: UseAnalyticsConfig) {
8
+ const { entityType, entityId, defaultTimerName, shouldTrackDefaultTimer } =
9
+ options;
10
+ const { sendEvent } = useDataIngestionApi();
11
+ const timers = useRef<Map<string, AnalyticsTimer>>(new Map());
12
+
13
+ useEffect(() => {
14
+ if (shouldTrackDefaultTimer) {
15
+ startTimer(defaultTimerName, {
16
+ entity_type: entityType,
17
+ entity_id: entityId,
18
+ });
19
+ }
20
+ }, []);
21
+
22
+ const withDefaults = useCallback(
23
+ (properties?: Record<string, unknown>) => ({
24
+ ...properties,
25
+ ...(entityId && { entity_id: entityId }),
26
+ ...(entityType && { entity_type: entityType }),
27
+ }),
28
+ [entityType, entityId]
29
+ );
30
+
31
+ /**
32
+ * Track a generic event
33
+ * @param eventName - Unique name for the event
34
+ * @param properties - Additional properties to track
35
+ */
36
+ const trackEvent = useCallback(
37
+ (eventName: string, properties?: Record<string, unknown>) => {
38
+ sendEvent({
39
+ event_name: eventName,
40
+ event_category: "click",
41
+ metadata: {
42
+ ...withDefaults(properties),
43
+ },
44
+ });
45
+ },
46
+ [sendEvent]
47
+ );
48
+
49
+ /**
50
+ * Track errors
51
+ * @param eventName - Name of the event where error occurred
52
+ * @param error - Error object or message
53
+ * @param context - Additional context about the error
54
+ */
55
+ const trackError = useCallback(
56
+ (
57
+ eventName: string,
58
+ error: Error | string,
59
+ context?: {
60
+ entityType?: string;
61
+ entityId?: string;
62
+ isNew?: boolean;
63
+ operation?: string;
64
+ }
65
+ ) => {
66
+ const errorMessage = typeof error === "string" ? error : error.message;
67
+ const errorStack = typeof error === "string" ? undefined : error.stack;
68
+
69
+ const errorData: Record<string, unknown> = {
70
+ error_message: errorMessage,
71
+ error_type: typeof error === "string" ? "string" : error.name,
72
+ error_stack: errorStack,
73
+ ...withDefaults(context),
74
+ is_new_entity: context?.isNew ?? false,
75
+ operation: context?.operation,
76
+ };
77
+
78
+ sendEvent({
79
+ event_name: eventName,
80
+ event_category: "error",
81
+ is_new: context?.isNew,
82
+ metadata: errorData,
83
+ });
84
+ },
85
+ [sendEvent]
86
+ );
87
+
88
+ /**
89
+ * Start a timer for tracking duration
90
+ * @param timerName - Unique name for the timer
91
+ * @param metadata - Optional metadata to store with the timer
92
+ */
93
+ const startTimer = useCallback(
94
+ (timerName: string, metadata?: Record<string, unknown>) => {
95
+ timers.current.set(timerName, {
96
+ startTime: Date.now(),
97
+ metadata,
98
+ });
99
+ },
100
+ [entityType, entityId]
101
+ );
102
+
103
+ /**
104
+ * End a timer and track the duration
105
+ * @param timerName - Name of the timer to end
106
+ * @returns Duration in milliseconds, or null if timer not found
107
+ */
108
+ const endTimer = useCallback(
109
+ (timerName: string): number | null => {
110
+ const timer = timers.current.get(timerName);
111
+
112
+ if (!timer) {
113
+ console.warn(`Timer "${timerName}" not found`);
114
+ return null;
115
+ }
116
+
117
+ const duration = Date.now() - timer.startTime;
118
+ timers.current.delete(timerName);
119
+
120
+ return duration;
121
+ },
122
+ [entityType, entityId]
123
+ );
124
+
125
+ /**
126
+ * Generic method: Track entity creation with custom event name
127
+ * @param eventName - Specific event name (e.g., ANALYTICS_EVENTS.BUDGET_CREATED)
128
+ * @param entityType - Type of entity (e.g., 'budget', 'buying_policy')
129
+ * @param properties - Additional properties to track
130
+ */
131
+ const trackEntityCreated = useCallback(
132
+ (
133
+ eventName: string,
134
+ entityType: string,
135
+ properties?: Record<string, unknown>
136
+ ) => {
137
+ const duration = endTimer(defaultTimerName);
138
+ if (duration) {
139
+ properties = {
140
+ ...withDefaults(properties),
141
+ ...(duration !== null && { time_to_create_ms: duration }),
142
+ };
143
+ }
144
+
145
+ sendEvent({
146
+ event_name: eventName,
147
+ event_category: "creation",
148
+ entity_type: entityType,
149
+ metadata: withDefaults(properties),
150
+ });
151
+ },
152
+ [sendEvent]
153
+ );
154
+
155
+ /**
156
+ * Generic method: Track entity editing with custom event name
157
+ * @param eventName - Specific event name (e.g., ANALYTICS_EVENTS.BUDGET_EDITED)
158
+ * @param entityType - Type of entity (e.g., 'budget', 'buying_policy')
159
+ * @param entityId - ID of the entity being edited
160
+ * @param properties - Additional properties to track
161
+ */
162
+ const trackEntityEdited = useCallback(
163
+ (eventName: string, properties?: Record<string, unknown>) => {
164
+ sendEvent({
165
+ event_name: eventName,
166
+ event_category: "edition",
167
+ entity_type: entityType,
168
+ entity_id: entityId,
169
+ metadata: withDefaults(properties),
170
+ });
171
+ },
172
+ [sendEvent]
173
+ );
174
+
175
+ /**
176
+ * Generic method: Track entity creation error with custom event name
177
+ * @param eventName - Specific error event name (e.g., ANALYTICS_EVENTS.BUDGET_CREATE_ERROR)
178
+ * @param entityType - Type of entity (e.g., 'budget', 'buying_policy')
179
+ * @param error - Error object or message
180
+ * @param properties - Additional properties to track
181
+ */
182
+ const trackEntityCreateError = useCallback(
183
+ (
184
+ eventName: string,
185
+ error: Error | string,
186
+ properties?: Record<string, unknown>
187
+ ) => {
188
+ const duration = endTimer(defaultTimerName);
189
+ if (duration) {
190
+ properties = {
191
+ ...withDefaults(properties),
192
+ ...(duration !== null && { time_to_error_ms: duration }),
193
+ };
194
+ }
195
+
196
+ trackError(eventName, error, {
197
+ entityType: entityType,
198
+ entityId: entityId,
199
+ isNew: true,
200
+ operation: "create",
201
+ ...withDefaults(properties),
202
+ });
203
+ },
204
+ [trackError, entityType, entityId]
205
+ );
206
+
207
+ /**
208
+ * Generic method: Track entity edit error with custom event name
209
+ * @param eventName - Specific error event name (e.g., ANALYTICS_EVENTS.BUDGET_EDIT_ERROR)
210
+ * @param entityType - Type of entity (e.g., 'budget', 'buying_policy')
211
+ * @param entityId - ID of the entity being edited
212
+ * @param error - Error object or message
213
+ * @param properties - Additional properties to track
214
+ */
215
+ const trackEntityEditError = useCallback(
216
+ (
217
+ eventName: string,
218
+ entityType: string,
219
+ entityId: string,
220
+ error: Error | string,
221
+ properties?: Record<string, unknown>
222
+ ) => {
223
+ trackError(eventName, error, {
224
+ entityType,
225
+ entityId,
226
+ isNew: false,
227
+ operation: "update",
228
+ ...withDefaults(properties),
229
+ });
230
+ },
231
+ [trackError]
232
+ );
233
+
234
+ return {
235
+ // Generic methods
236
+ trackEvent,
237
+ trackError,
238
+
239
+ // Timer methods
240
+ startTimer,
241
+ endTimer,
242
+
243
+ // Entity-specific generic methods (for Budget, Buying Policy, etc.)
244
+ trackEntityCreated,
245
+ trackEntityEdited,
246
+ trackEntityCreateError,
247
+ trackEntityEditError,
248
+ };
249
+ }
@@ -14,3 +14,4 @@ export { usePageItems, type UsePageItemsProps } from "./usePageItems";
14
14
  export { useRouterLoading } from "./useRouterLoading";
15
15
  export { useLogger } from "./useLogger";
16
16
  export { useGetDependenciesVersion } from "./useGetDependenciesVersion";
17
+ export { useAnalytics } from "./analytics/useAnalytics";
@@ -0,0 +1,101 @@
1
+ import storeConfig from "discovery.config";
2
+
3
+ import { isDevelopment } from "../../../utils/environment";
4
+
5
+ import type { DataIngestionApiFields, DataIngestionApiParams } from "./types";
6
+
7
+ /**
8
+ * VTEX Data Ingestion API for Analytics
9
+ * https://internal-docs.vtex.com/Analytics/VTEX-Data-Platform/data-ingestion-API/
10
+ */
11
+
12
+ export const APP_NAME = "b2b-organization-account";
13
+
14
+ /**
15
+ * Send event to VTEX Data Ingestion API
16
+ */
17
+ export async function dataIngestionApi(
18
+ fields: DataIngestionApiParams
19
+ ): Promise<void> {
20
+ const isDevEnvironment = fields.workspace !== "master";
21
+
22
+ delete fields.workspace;
23
+
24
+ const isInsideVtexNetwork = false;
25
+ const endpoint = isInsideVtexNetwork
26
+ ? "https://analytics.vtex.com/api/analytics/schemaless-events"
27
+ : "https://rc.vtex.com/api/analytics/schemaless-events";
28
+
29
+ try {
30
+ await fetch(endpoint, {
31
+ method: "POST",
32
+ headers: {
33
+ "Content-Type": "application/json",
34
+ },
35
+ body: JSON.stringify({
36
+ name: APP_NAME,
37
+ ...fields,
38
+ }),
39
+ });
40
+ } catch (error) {
41
+ if (isDevEnvironment) {
42
+ console.warn(
43
+ `Error when sending analytics data for "${fields.event_name}".`,
44
+ error
45
+ );
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get common analytics context
52
+ */
53
+ function getCommonAnalyticsContext() {
54
+ const account = storeConfig?.api?.storeId || "unknown";
55
+ const locale =
56
+ typeof window !== "undefined"
57
+ ? window.navigator.language || "undefined"
58
+ : "undefined";
59
+ const production = !isDevelopment();
60
+
61
+ // Detect device type
62
+ let device = "desktop";
63
+ if (typeof window !== "undefined") {
64
+ const userAgent = window.navigator.userAgent.toLowerCase();
65
+ device =
66
+ /mobile|android|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(
67
+ userAgent
68
+ )
69
+ ? "mobile"
70
+ : "desktop";
71
+ }
72
+
73
+ return {
74
+ account,
75
+ locale,
76
+ production,
77
+ device,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Hook to send analytics events
83
+ * This is a simple version that will be extended by useAnalytics hook
84
+ */
85
+ export function useDataIngestionApi() {
86
+ const commonFields = getCommonAnalyticsContext();
87
+
88
+ const sendEvent = (fields: DataIngestionApiFields) => {
89
+ // Get workspace from environment
90
+ const workspace = isDevelopment() ? "dev" : "master";
91
+
92
+ dataIngestionApi({
93
+ ...commonFields,
94
+ event_category: fields.event_category || "click",
95
+ ...fields,
96
+ workspace,
97
+ });
98
+ };
99
+
100
+ return { sendEvent };
101
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Event name constants for consistent tracking
3
+ */
4
+ export const ANALYTICS_EVENTS = {
5
+ // Budget events
6
+ BUDGET_CREATED: "budget_created",
7
+ BUDGET_EDITED: "budget_edited",
8
+ BUDGET_DELETED: "budget_deleted",
9
+ BUDGET_CREATE_ERROR: "budget_create_error",
10
+ BUDGET_EDIT_ERROR: "budget_edit_error",
11
+ BUDGET_DELETE_ERROR: "budget_delete_error",
12
+ BUDGET_STEP_TIMING: "budget_step_timing",
13
+ BUDGET_FLOW_COMPLETED: "budget_flow_completed",
14
+
15
+ // Buying Policy events
16
+ BUYING_POLICY_CREATED: "buying_policy_created",
17
+ BUYING_POLICY_EDITED: "buying_policy_edited",
18
+ BUYING_POLICY_DELETED: "buying_policy_deleted",
19
+ BUYING_POLICY_CREATE_ERROR: "buying_policy_create_error",
20
+ BUYING_POLICY_EDIT_ERROR: "buying_policy_edit_error",
21
+ BUYING_POLICY_DELETE_ERROR: "buying_policy_delete_error",
22
+ BUYING_POLICY_STEP_TIMING: "buying_policy_step_timing",
23
+ BUYING_POLICY_FLOW_COMPLETED: "buying_policy_flow_completed",
24
+
25
+ // Custom Field events
26
+ CUSTOM_FIELD_CREATED: "custom_field_created",
27
+ CUSTOM_FIELD_EDITED: "custom_field_edited",
28
+ CUSTOM_FIELD_DELETED: "custom_field_deleted",
29
+ CUSTOM_FIELD_CREATE_ERROR: "custom_field_create_error",
30
+ CUSTOM_FIELD_EDIT_ERROR: "custom_field_edit_error",
31
+ CUSTOM_FIELD_DELETE_ERROR: "custom_field_delete_error",
32
+
33
+ // Address events
34
+ ADDRESS_CREATED: "address_created",
35
+ ADDRESS_EDITED: "address_edited",
36
+ ADDRESS_DELETED: "address_deleted",
37
+ ADDRESS_CREATE_ERROR: "address_create_error",
38
+ ADDRESS_EDIT_ERROR: "address_edit_error",
39
+ ADDRESS_DELETE_ERROR: "address_delete_error",
40
+ ADDRESS_STEP_TIMING: "address_step_timing",
41
+
42
+ // Location events
43
+ LOCATION_CREATED: "location_created",
44
+ LOCATION_EDITED: "location_edited",
45
+ LOCATION_DELETED: "location_deleted",
46
+ LOCATION_CREATE_ERROR: "location_create_error",
47
+ LOCATION_EDIT_ERROR: "location_edit_error",
48
+ LOCATION_DELETE_ERROR: "location_delete_error",
49
+
50
+ // Recipient events
51
+ RECIPIENT_CREATED: "recipient_created",
52
+ RECIPIENT_EDITED: "recipient_edited",
53
+ RECIPIENT_DELETED: "recipient_deleted",
54
+ RECIPIENT_CREATE_ERROR: "recipient_create_error",
55
+ RECIPIENT_EDIT_ERROR: "recipient_edit_error",
56
+ RECIPIENT_DELETE_ERROR: "recipient_delete_error",
57
+
58
+ // User events
59
+ USER_CREATED: "user_created",
60
+ USER_EDITED: "user_edited",
61
+ USER_DELETED: "user_deleted",
62
+ USER_CREATE_ERROR: "user_create_error",
63
+ USER_EDIT_ERROR: "user_edit_error",
64
+ USER_DELETE_ERROR: "user_delete_error",
65
+
66
+ // Organization Unit events
67
+ ORG_UNIT_CREATED: "org_unit_created",
68
+ ORG_UNIT_EDITED: "org_unit_edited",
69
+ ORG_UNIT_DELETED: "org_unit_deleted",
70
+ ORG_UNIT_CREATE_ERROR: "org_unit_create_error",
71
+ ORG_UNIT_EDIT_ERROR: "org_unit_edit_error",
72
+ ORG_UNIT_DELETE_ERROR: "org_unit_delete_error",
73
+
74
+ // Interaction events
75
+ TOOLTIP_INTERACTION: "tooltip_interaction",
76
+ STEP_CHANGE: "step_change",
77
+
78
+ // Generic events
79
+ ENTITY_CREATED: "entity_created",
80
+ ENTITY_EDITED: "entity_edited",
81
+ ENTITY_ERROR: "entity_error",
82
+ TIMER_COMPLETED: "timer_completed",
83
+ } as const;