@vtex/faststore-plugin-buyer-portal 1.3.26 → 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 (35) hide show
  1. package/CHANGELOG.md +15 -1
  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/BasicBuyingPolicyDrawer/basic-buying-policy-drawer.scss +25 -18
  17. package/src/features/buying-policies/components/DeleteBuyingPolicyDrawer/DeleteBuyingPolicyDrawer.tsx +28 -2
  18. package/src/features/buying-policies/components/UpdateBuyingPolicyDrawer/UpdateBuyingPolicyDrawer.tsx +49 -5
  19. package/src/features/custom-fields/components/CreateCustomFieldValueDrawer/CreateCustomFieldValueDrawer.tsx +60 -5
  20. package/src/features/custom-fields/components/DeleteCustomFieldValueDrawer/DeleteCustomFieldValueDrawer.tsx +61 -5
  21. package/src/features/custom-fields/components/UpdateCustomFieldValueDrawer/UpdateCustomFieldValueDrawer.tsx +32 -3
  22. package/src/features/org-units/components/CreateOrgUnitDrawer/CreateOrgUnitDrawer.tsx +18 -0
  23. package/src/features/org-units/components/DeleteOrgUnitDrawer/DeleteOrgUnitDrawer.tsx +28 -0
  24. package/src/features/org-units/components/UpdateOrgUnitDrawer/UpdateOrgUnitDrawer.tsx +28 -0
  25. package/src/features/shared/hooks/analytics/types.ts +14 -0
  26. package/src/features/shared/hooks/analytics/useAnalytics.ts +249 -0
  27. package/src/features/shared/hooks/index.ts +1 -0
  28. package/src/features/shared/services/logger/analytics/analytics.ts +101 -0
  29. package/src/features/shared/services/logger/analytics/constants.ts +83 -0
  30. package/src/features/shared/services/logger/analytics/types.ts +108 -0
  31. package/src/features/shared/services/logger/index.ts +1 -0
  32. package/src/features/shared/utils/constants.ts +1 -1
  33. package/src/features/users/components/CreateUserDrawer/CreateUserDrawer.tsx +24 -0
  34. package/src/features/users/components/DeleteUserDrawer/DeleteUserDrawer.tsx +23 -2
  35. package/src/features/users/components/UpdateUserDrawer/UpdateUserDrawer.tsx +45 -4
@@ -2,11 +2,12 @@ import { useUI } from "@faststore/ui";
2
2
 
3
3
  import { BasicDrawerProps } from "../../../shared/components";
4
4
  import { DeleteEntityDrawer } from "../../../shared/components/CustomField/delete-custom-field/DeleteCustomFieldDrawer";
5
- import { useBuyerPortal } from "../../../shared/hooks";
5
+ import { useAnalytics, useBuyerPortal } from "../../../shared/hooks";
6
6
  import {
7
7
  useDeleteCustomFieldValue,
8
8
  useDeleteCustomFieldValueToUnitScope,
9
9
  } from "../../../shared/hooks/custom-field";
10
+ import { ANALYTICS_EVENTS } from "../../../shared/services/logger/analytics/constants";
10
11
  import { CustomFieldData } from "../../types";
11
12
 
12
13
  export interface DeleteCustomFieldValueDrawerProps
@@ -36,12 +37,29 @@ export function DeleteCustomFieldValueDrawer({
36
37
  const {
37
38
  clientContext: { cookie },
38
39
  } = useBuyerPortal();
40
+ const { trackEvent } = useAnalytics({
41
+ entityType: "custom_field",
42
+ entityId: currentData?.id,
43
+ defaultTimerName: "custom_field_delete",
44
+ shouldTrackDefaultTimer: !!currentData,
45
+ });
46
+
39
47
  const {
40
48
  mutate: deleteCustomFieldValueToUnitScope,
41
49
  isLoading: isLoadingDeleteCustomFieldValueToUnitScope,
42
50
  } = useDeleteCustomFieldValueToUnitScope({
43
51
  options: {
44
52
  onSuccess: () => {
53
+ trackEvent(ANALYTICS_EVENTS.CUSTOM_FIELD_DELETED, {
54
+ custom_field_type: customField,
55
+ custom_field_id: currentData?.id,
56
+ custom_field_name: currentData?.value,
57
+ delete_mode: "unitScope",
58
+ org_unit_id: unitId,
59
+ contract_id: contractId,
60
+ entity_type: "custom_field",
61
+ });
62
+
45
63
  pushToast({
46
64
  message: `${customField} removed successfully`,
47
65
  status: "INFO",
@@ -50,11 +68,25 @@ export function DeleteCustomFieldValueDrawer({
50
68
  close();
51
69
  refetch();
52
70
  },
53
- onError: (error) =>
71
+ onError: (error) => {
72
+ const errorMessage = typeof error === "string" ? error : error.message;
73
+
74
+ trackEvent(ANALYTICS_EVENTS.CUSTOM_FIELD_DELETE_ERROR, {
75
+ entity_type: "custom_field",
76
+ entity_id: currentData?.id,
77
+ custom_field_type: customField,
78
+ custom_field_name: currentData?.value,
79
+ delete_mode: "unitScope",
80
+ org_unit_id: unitId,
81
+ contract_id: contractId,
82
+ error_message: errorMessage,
83
+ });
84
+
54
85
  pushToast({
55
86
  message: error.message,
56
87
  status: "ERROR",
57
- }),
88
+ });
89
+ },
58
90
  },
59
91
  });
60
92
 
@@ -64,6 +96,16 @@ export function DeleteCustomFieldValueDrawer({
64
96
  } = useDeleteCustomFieldValue({
65
97
  options: {
66
98
  onSuccess: () => {
99
+ trackEvent(ANALYTICS_EVENTS.CUSTOM_FIELD_DELETED, {
100
+ custom_field_type: customField,
101
+ custom_field_id: currentData?.id,
102
+ custom_field_name: currentData?.value,
103
+ delete_mode: "contract",
104
+ org_unit_id: unitId,
105
+ contract_id: contractId,
106
+ entity_type: "custom_field",
107
+ });
108
+
67
109
  pushToast({
68
110
  message: `${customField} deleted successfully`,
69
111
  status: "INFO",
@@ -72,11 +114,25 @@ export function DeleteCustomFieldValueDrawer({
72
114
  close();
73
115
  refetch();
74
116
  },
75
- onError: (error) =>
117
+ onError: (error) => {
118
+ const errorMessage = typeof error === "string" ? error : error.message;
119
+
120
+ trackEvent(ANALYTICS_EVENTS.CUSTOM_FIELD_DELETE_ERROR, {
121
+ entity_type: "custom_field",
122
+ entity_id: currentData?.id,
123
+ custom_field_type: customField,
124
+ custom_field_name: currentData?.value,
125
+ delete_mode: "contract",
126
+ org_unit_id: unitId,
127
+ contract_id: contractId,
128
+ error_message: errorMessage,
129
+ });
130
+
76
131
  pushToast({
77
132
  message: error.message,
78
133
  status: "ERROR",
79
- }),
134
+ });
135
+ },
80
136
  },
81
137
  });
82
138
 
@@ -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";