@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
@@ -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;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Analytics event categories
3
+ */
4
+ export type EventCategory =
5
+ | "click"
6
+ | "view"
7
+ | "apiAnswer"
8
+ | "webVitals"
9
+ | "creation"
10
+ | "edition"
11
+ | "error"
12
+ | "timing"
13
+ | "interaction";
14
+
15
+ /**
16
+ * Analytics event fields
17
+ */
18
+ export interface DataIngestionApiFields {
19
+ /** The type of the event */
20
+ event_category?: EventCategory;
21
+ /** The name of the event. Has to be unique through the app. */
22
+ event_name: string;
23
+ /** Additional informations */
24
+ /** Time spent on page */
25
+ time_on_page?: number;
26
+ /** Additional metadata about the event */
27
+ metadata?: Record<string, unknown>;
28
+ /** Entity type (e.g., 'budget', 'buying_policy') */
29
+ entity_type?: string;
30
+ /** Entity ID */
31
+ entity_id?: string;
32
+ /** Is this a new entity? */
33
+ is_new?: boolean;
34
+ /** Duration in milliseconds */
35
+ duration_ms?: number;
36
+ /** Step name for multi-step flows */
37
+ step_name?: string;
38
+ /** Flow name for multi-step flows */
39
+ flow_name?: string;
40
+ /** Component type for interactions */
41
+ component_type?: string;
42
+ /** Action performed */
43
+ action?: string;
44
+ }
45
+
46
+ /**
47
+ * Internal params for data ingestion API
48
+ */
49
+ export interface DataIngestionApiParams {
50
+ [key: string]: string | number | boolean | object | undefined;
51
+ workspace?: string;
52
+ }
53
+
54
+ /**
55
+ * Step timing properties
56
+ */
57
+ export interface StepTimingProperties {
58
+ flow_name: string;
59
+ step_name: string;
60
+ from_step?: string;
61
+ to_step?: string;
62
+ duration_ms: number;
63
+ step_index?: number;
64
+ total_steps?: number;
65
+ }
66
+
67
+ /**
68
+ * Error tracking properties
69
+ */
70
+ export interface ErrorEventProperties {
71
+ error_message: string;
72
+ error_type?: string;
73
+ error_stack?: string;
74
+ entity_type?: string;
75
+ entity_id?: string;
76
+ is_new_entity: boolean;
77
+ operation?: string; // create, update, delete
78
+ }
79
+
80
+ /**
81
+ * Interaction properties
82
+ */
83
+ export interface InteractionEventProperties {
84
+ component_type: string;
85
+ component_id?: string;
86
+ action: string;
87
+ selected_option?: string;
88
+ metadata?: Record<string, unknown>;
89
+ }
90
+
91
+ /**
92
+ * Generic entity creation properties
93
+ */
94
+ export interface EntityCreatedProperties {
95
+ entity_type: string;
96
+ entity_id?: string;
97
+ properties: Record<string, unknown>;
98
+ }
99
+
100
+ /**
101
+ * Generic entity edition properties
102
+ */
103
+ export interface EntityEditedProperties {
104
+ entity_type: string;
105
+ entity_id: string;
106
+ changed_fields?: string[];
107
+ properties: Record<string, unknown>;
108
+ }
@@ -2,3 +2,4 @@ export * from "./types";
2
2
  export * from "./constants";
3
3
  export * from "./context";
4
4
  export * from "./otlp-logger.service";
5
+ export * from "./analytics/analytics";
@@ -13,4 +13,4 @@ export const LOCAL_STORAGE_LOCATION_EDIT_KEY = "bp_hide_edit_location_confirm";
13
13
  export const LOCAL_STORAGE_RECIPIENT_EDIT_KEY =
14
14
  "bp_hide_edit_recipient_confirm";
15
15
 
16
- export const CURRENT_VERSION = "1.3.26";
16
+ export const CURRENT_VERSION = "1.3.28";
@@ -12,6 +12,8 @@ import {
12
12
  Icon,
13
13
  InputText,
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 { maskPhoneNumber } from "../../../shared/utils/phoneNumber";
17
19
  import { useAddUserToOrgUnit } from "../../hooks";
@@ -31,6 +33,11 @@ export const CreateUserDrawer = ({
31
33
  }: CreateUserDrawerProps) => {
32
34
  const { pushToast } = useUI();
33
35
  const router = useRouter();
36
+ const { trackEntityCreated, trackEntityCreateError } = useAnalytics({
37
+ entityType: "user",
38
+ defaultTimerName: "user_creation",
39
+ shouldTrackDefaultTimer: true,
40
+ });
34
41
  const [drawerState, setDrawerState] = useState<
35
42
  "CREATE_USER" | "USER_ALREADY_EXISTS"
36
43
  >("CREATE_USER");
@@ -74,6 +81,15 @@ export const CreateUserDrawer = ({
74
81
  };
75
82
 
76
83
  const handleAddUserSuccess = ({ user }: { user: { id: string } }) => {
84
+ trackEntityCreated(ANALYTICS_EVENTS.USER_CREATED, "user", {
85
+ user_name: name,
86
+ user_email: email,
87
+ user_phone: phone,
88
+ roles_count: roles.length,
89
+ role_ids: roles,
90
+ org_unit_id: orgUnitId,
91
+ });
92
+
77
93
  pushToast({
78
94
  message: "User successfully added",
79
95
  status: "INFO",
@@ -112,6 +128,14 @@ export const CreateUserDrawer = ({
112
128
  };
113
129
  };
114
130
 
131
+ trackEntityCreateError(ANALYTICS_EVENTS.USER_CREATE_ERROR, err, {
132
+ org_unit_id: orgUnitId,
133
+ error_type:
134
+ error.code === "USER_ALREADY_EXISTS"
135
+ ? "user_already_exists"
136
+ : "unknown",
137
+ });
138
+
115
139
  setUserAlreadyInUse({
116
140
  ...error.user,
117
141
  orgUnit: error.orgUnit,
@@ -8,6 +8,8 @@ import {
8
8
  InputText,
9
9
  type BasicDrawerProps,
10
10
  } from "../../../shared/components";
11
+ import { useAnalytics } from "../../../shared/hooks";
12
+ import { ANALYTICS_EVENTS } from "../../../shared/services/logger/analytics/constants";
11
13
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
12
14
  import { useRemoveUserFromOrgUnit } from "../../hooks";
13
15
 
@@ -26,10 +28,22 @@ export const DeleteUserDrawer = ({
26
28
  const [userNameConfirmation, setUserNameConfirmation] = useState("");
27
29
 
28
30
  const { pushToast } = useUI();
29
-
30
31
  const { orgUnit } = useOrgUnitByUser(user.id);
32
+ const { trackEvent } = useAnalytics({
33
+ entityType: "user",
34
+ entityId: user.id,
35
+ defaultTimerName: "user_delete",
36
+ shouldTrackDefaultTimer: true,
37
+ });
31
38
 
32
39
  const handleRemoveSuccess = () => {
40
+ trackEvent(ANALYTICS_EVENTS.USER_DELETED, {
41
+ user_id: user.id,
42
+ user_name: user.name,
43
+ org_unit_id: orgUnit?.id,
44
+ entity_type: "user",
45
+ });
46
+
33
47
  pushToast({
34
48
  message: "User successfully deleted",
35
49
  status: "INFO",
@@ -52,7 +66,14 @@ export const DeleteUserDrawer = ({
52
66
  const { removeUserFromOrgUnit, isRemoveUserFromOrgUnitLoading } =
53
67
  useRemoveUserFromOrgUnit({
54
68
  onSuccess: handleRemoveSuccess,
55
- onError: () => {
69
+ onError: (error) => {
70
+ trackEvent(ANALYTICS_EVENTS.USER_DELETE_ERROR, {
71
+ entity_id: user.id,
72
+ user_name: user.name,
73
+ error_message: error instanceof Error ? error.message : String(error),
74
+ org_unit_id: orgUnit?.id,
75
+ });
76
+
56
77
  pushToast({
57
78
  message: "An error occurred while deleting the user",
58
79
  status: "ERROR",
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useRef, useState } from "react";
2
2
 
3
3
  import { Skeleton, useUI } from "@faststore/ui";
4
4
 
@@ -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 { maskPhoneNumber } from "../../../shared/utils/phoneNumber";
14
16
  import { useGetUserById, useUpdateUser } from "../../hooks";
15
17
 
@@ -29,16 +31,25 @@ export const UpdateUserDrawer = ({
29
31
  ...props
30
32
  }: UpdateUserDrawerProps) => {
31
33
  const { pushToast } = useUI();
34
+ const { trackEntityEdited, trackEntityEditError } = useAnalytics({
35
+ entityType: "user",
36
+ entityId: userId,
37
+ defaultTimerName: "user_edit",
38
+ shouldTrackDefaultTimer: true,
39
+ });
40
+ const initialRolesRef = useRef<string[]>([]);
32
41
 
33
42
  const { isUserLoading } = useGetUserById(
34
43
  { orgUnitId, userId },
35
44
  {
36
45
  onSuccess: (data) => {
46
+ const userRoles = data?.roles ? data.roles : [];
47
+ initialRolesRef.current = userRoles;
37
48
  setForm({
38
49
  name: data?.name ?? "",
39
50
  email: data?.email ?? "",
40
51
  phone: data?.phone ?? "",
41
- roles: data?.roles ? data.roles : [],
52
+ roles: userRoles,
42
53
  });
43
54
  },
44
55
  onError: () => {
@@ -74,6 +85,16 @@ export const UpdateUserDrawer = ({
74
85
  };
75
86
 
76
87
  const handleUpdateUserSuccess = () => {
88
+ trackEntityEdited(ANALYTICS_EVENTS.USER_EDITED, {
89
+ user_id: userId,
90
+ user_name: name,
91
+ user_phone: phone,
92
+ old_roles: initialRolesRef.current,
93
+ new_roles: roles,
94
+ roles_count: roles.length,
95
+ org_unit_id: orgUnitId,
96
+ });
97
+
77
98
  pushToast({
78
99
  message: "User successfully updated",
79
100
  status: "INFO",
@@ -84,7 +105,17 @@ export const UpdateUserDrawer = ({
84
105
 
85
106
  const { updateUser, isUpdateUserLoading } = useUpdateUser({
86
107
  onSuccess: handleUpdateUserSuccess,
87
- onError: () => {
108
+ onError: (error) => {
109
+ trackEntityEditError(
110
+ ANALYTICS_EVENTS.USER_EDIT_ERROR,
111
+ "user",
112
+ userId,
113
+ error,
114
+ {
115
+ org_unit_id: orgUnitId,
116
+ }
117
+ );
118
+
88
119
  pushToast({
89
120
  message: "Failed to update user",
90
121
  status: "ERROR",
@@ -94,7 +125,17 @@ export const UpdateUserDrawer = ({
94
125
 
95
126
  const { updateRoles, isUpdateRolesLoading } = useUpdateRoles({
96
127
  onSuccess: handleUpdateUserSuccess,
97
- onError: () => {
128
+ onError: (error) => {
129
+ trackEntityEditError(
130
+ ANALYTICS_EVENTS.USER_EDIT_ERROR,
131
+ "user",
132
+ userId,
133
+ error,
134
+ {
135
+ org_unit_id: orgUnitId,
136
+ }
137
+ );
138
+
98
139
  pushToast({
99
140
  message: "Failed to update user",
100
141
  status: "ERROR",