@vtex/faststore-plugin-buyer-portal 1.3.27 → 1.3.29
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 +16 -2
- 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/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
- package/src/themes/colors.scss +10 -0
- package/src/themes/layouts.scss +4 -0
- package/src/themes/tokens.scss +169 -0
- package/src/themes/typography.scss +76 -0
|
@@ -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;
|