@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.
- package/CHANGELOG.md +15 -1
- package/package.json +1 -1
- package/src/features/addresses/components/AddRecipientsDrawer/AddRecipientsDrawer.tsx +20 -2
- package/src/features/addresses/components/CreateAddressDrawer/CreateAddressDrawer.tsx +50 -4
- package/src/features/addresses/components/DeleteAddressDrawer/DeleteAddressDrawer.tsx +26 -2
- package/src/features/addresses/components/DeleteAddressLocationDrawer/DeleteAddressLocationDrawer.tsx +29 -3
- package/src/features/addresses/components/DeleteRecipientAddressDrawer/DeleteRecipientAddressDrawer.tsx +24 -2
- package/src/features/addresses/components/EditAddressDrawer/EditAddressDrawer.tsx +28 -2
- package/src/features/addresses/components/EditAddressLocationDrawer/EditAddressLocationDrawer.tsx +30 -4
- package/src/features/addresses/components/EditRecipientAddressDrawer/EditRecipientAddressDrawer.tsx +30 -2
- package/src/features/addresses/components/LocationsDrawer/LocationsDrawer.tsx +29 -2
- package/src/features/budgets/components/BudgetDeleteDrawer/BudgetDeleteDrawer.tsx +32 -4
- package/src/features/budgets/components/CreateBudgetDrawer/CreateBudgetDrawer.tsx +59 -4
- package/src/features/budgets/components/EditBudgetDrawer/EditBudgetDrawer.tsx +28 -1
- package/src/features/buying-policies/components/AddBuyingPolicyDrawer/AddBuyingPolicyDrawer.tsx +48 -5
- package/src/features/buying-policies/components/BasicBuyingPolicyDrawer/basic-buying-policy-drawer.scss +25 -18
- package/src/features/buying-policies/components/DeleteBuyingPolicyDrawer/DeleteBuyingPolicyDrawer.tsx +28 -2
- package/src/features/buying-policies/components/UpdateBuyingPolicyDrawer/UpdateBuyingPolicyDrawer.tsx +49 -5
- package/src/features/custom-fields/components/CreateCustomFieldValueDrawer/CreateCustomFieldValueDrawer.tsx +60 -5
- package/src/features/custom-fields/components/DeleteCustomFieldValueDrawer/DeleteCustomFieldValueDrawer.tsx +61 -5
- package/src/features/custom-fields/components/UpdateCustomFieldValueDrawer/UpdateCustomFieldValueDrawer.tsx +32 -3
- package/src/features/org-units/components/CreateOrgUnitDrawer/CreateOrgUnitDrawer.tsx +18 -0
- package/src/features/org-units/components/DeleteOrgUnitDrawer/DeleteOrgUnitDrawer.tsx +28 -0
- package/src/features/org-units/components/UpdateOrgUnitDrawer/UpdateOrgUnitDrawer.tsx +28 -0
- package/src/features/shared/hooks/analytics/types.ts +14 -0
- package/src/features/shared/hooks/analytics/useAnalytics.ts +249 -0
- package/src/features/shared/hooks/index.ts +1 -0
- package/src/features/shared/services/logger/analytics/analytics.ts +101 -0
- package/src/features/shared/services/logger/analytics/constants.ts +83 -0
- package/src/features/shared/services/logger/analytics/types.ts +108 -0
- package/src/features/shared/services/logger/index.ts +1 -0
- package/src/features/shared/utils/constants.ts +1 -1
- package/src/features/users/components/CreateUserDrawer/CreateUserDrawer.tsx +24 -0
- package/src/features/users/components/DeleteUserDrawer/DeleteUserDrawer.tsx +23 -2
- 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";
|