diff --git a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx index 3a0eebf33a34..83221396a6f1 100644 --- a/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx +++ b/airbyte-webapp/src/components/CreateConnectionContent/CreateConnectionContent.tsx @@ -6,9 +6,10 @@ import { IDataItem } from "components/base/DropDown/components/Option"; import { JobItem } from "components/JobItem/JobItem"; import LoadingSchema from "components/LoadingSchema"; +import { Action, Namespace } from "core/analytics"; import { LogsRequestError } from "core/request/LogsRequestError"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { useCreateConnection, ValuesProps } from "hooks/services/useConnectionHook"; -import { TrackActionLegacyType, TrackActionType, TrackActionNamespace, useTrackAction } from "hooks/useTrackAction"; import ConnectionForm from "views/Connection/ConnectionForm"; import { ConnectionFormProps } from "views/Connection/ConnectionForm/ConnectionForm"; import { FormikConnectionFormValues } from "views/Connection/ConnectionForm/formConfig"; @@ -40,10 +41,7 @@ const CreateConnectionContent: React.FC = ({ additionBottomControls, }) => { const { mutateAsync: createConnection } = useCreateConnection(); - const trackNewConnectionAction = useTrackAction( - TrackActionNamespace.CONNECTION, - TrackActionLegacyType.NEW_CONNECTION - ); + const analyticsService = useAnalyticsService(); const { schema, isLoading, schemaErrorStatus, catalogId, onDiscoverSchema } = useDiscoverSchema(source.sourceId); @@ -90,7 +88,8 @@ const CreateConnectionContent: React.FC = ({ const enabledStreams = connection.syncCatalog.streams.filter((stream) => stream.config?.selected).length; if (item) { - trackNewConnectionAction("Select a frequency", TrackActionType.FREQUENCY, { + analyticsService.track(Namespace.CONNECTION, Action.FREQUENCY, { + actionDescription: "Frequency selected", frequency: item.label, connector_source_definition: source?.sourceName, connector_source_definition_id: source?.sourceDefinitionId, diff --git a/airbyte-webapp/src/components/EntityTable/hooks.tsx b/airbyte-webapp/src/components/EntityTable/hooks.tsx index fefbe74624fa..46d6091d2482 100644 --- a/airbyte-webapp/src/components/EntityTable/hooks.tsx +++ b/airbyte-webapp/src/components/EntityTable/hooks.tsx @@ -1,7 +1,8 @@ import { getFrequencyConfig } from "config/utils"; +import { Action, Namespace } from "core/analytics"; import { buildConnectionUpdate } from "core/domain/connection"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { useSyncConnection, useUpdateConnection } from "hooks/services/useConnectionHook"; -import { TrackActionLegacyType, TrackActionType, TrackActionNamespace, useTrackAction } from "hooks/useTrackAction"; import { ConnectionStatus, WebBackendConnectionRead } from "../../core/request/AirbyteClient"; @@ -11,7 +12,7 @@ const useSyncActions = (): { } => { const { mutateAsync: updateConnection } = useUpdateConnection(); const { mutateAsync: syncConnection } = useSyncConnection(); - const trackSourceAction = useTrackAction(TrackActionNamespace.CONNECTION, TrackActionLegacyType.SOURCE); + const analyticsService = useAnalyticsService(); const changeStatus = async (connection: WebBackendConnectionRead) => { await updateConnection( @@ -24,16 +25,13 @@ const useSyncActions = (): { const enabledStreams = connection.syncCatalog.streams.filter((stream) => stream.config?.selected).length; - const trackableAction = - connection.status === ConnectionStatus.active ? TrackActionType.DISABLE : TrackActionType.REENABLE; + const trackableAction = connection.status === ConnectionStatus.active ? Action.DISABLE : Action.REENABLE; - const trackableActionString = `${trackableAction} connection`; - - trackSourceAction(trackableActionString, trackableAction, { + analyticsService.track(Namespace.CONNECTION, trackableAction, { frequency: frequency?.type, connector_source: connection.source?.sourceName, connector_source_definition_id: connection.source?.sourceDefinitionId, - connector_destination: connection.destination?.name, + connector_destination: connection.destination?.destinationName, connector_destination_definition_id: connection.destination?.destinationDefinitionId, available_streams: connection.syncCatalog.streams.length, enabled_streams: enabledStreams, diff --git a/airbyte-webapp/src/core/analytics/AnalyticsService.test.ts b/airbyte-webapp/src/core/analytics/AnalyticsService.test.ts new file mode 100644 index 000000000000..2edf85729ad3 --- /dev/null +++ b/airbyte-webapp/src/core/analytics/AnalyticsService.test.ts @@ -0,0 +1,57 @@ +import { AnalyticsService } from "./AnalyticsService"; +import { Action, Namespace } from "./types"; + +describe("AnalyticsService", () => { + beforeEach(() => { + window.analytics = { + track: jest.fn(), + alias: jest.fn(), + group: jest.fn(), + identify: jest.fn(), + page: jest.fn(), + reset: jest.fn(), + }; + }); + + it("should send events to segment", () => { + const service = new AnalyticsService({}); + service.track(Namespace.CONNECTION, Action.CREATE, {}); + expect(window.analytics.track).toHaveBeenCalledWith("Airbyte.UI.Connection.Create", expect.anything()); + }); + + it("should send version and environment for prod", () => { + const service = new AnalyticsService({}, "0.42.13"); + service.track(Namespace.CONNECTION, Action.CREATE, {}); + expect(window.analytics.track).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ environment: "prod", airbyte_version: "0.42.13" }) + ); + }); + + it("should send version and environment for dev", () => { + const service = new AnalyticsService({}, "dev"); + service.track(Namespace.CONNECTION, Action.CREATE, {}); + expect(window.analytics.track).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ environment: "dev", airbyte_version: "dev" }) + ); + }); + + it("should pass parameters to segment event", () => { + const service = new AnalyticsService({}); + service.track(Namespace.CONNECTION, Action.CREATE, { actionDescription: "Created new connection" }); + expect(window.analytics.track).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ actionDescription: "Created new connection" }) + ); + }); + + it("should pass context parameters to segment event", () => { + const service = new AnalyticsService({ context: 42 }); + service.track(Namespace.CONNECTION, Action.CREATE, { actionDescription: "Created new connection" }); + expect(window.analytics.track).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ actionDescription: "Created new connection", context: 42 }) + ); + }); +}); diff --git a/airbyte-webapp/src/core/analytics/AnalyticsService.ts b/airbyte-webapp/src/core/analytics/AnalyticsService.ts index 125a1887bb89..9388330bad63 100644 --- a/airbyte-webapp/src/core/analytics/AnalyticsService.ts +++ b/airbyte-webapp/src/core/analytics/AnalyticsService.ts @@ -1,4 +1,4 @@ -import { SegmentAnalytics } from "./types"; +import { Action, EventParams, Namespace, SegmentAnalytics } from "./types"; export class AnalyticsService { constructor(private context: Record, private version?: string) {} @@ -11,13 +11,14 @@ export class AnalyticsService { reset = (): void => this.getSegmentAnalytics()?.reset?.(); - track =

>(name: string, properties: P): void => - this.getSegmentAnalytics()?.track?.(name, { - ...properties, + track = (namespace: Namespace, action: Action, params: EventParams & { actionDescription?: string }) => { + this.getSegmentAnalytics()?.track(`Airbyte.UI.${namespace}.${action}`, { + ...params, ...this.context, airbyte_version: this.version, environment: this.version === "dev" ? "dev" : "prod", }); + }; identify = (userId: string, traits: Record = {}): void => { this.getSegmentAnalytics()?.identify?.(userId, traits); diff --git a/airbyte-webapp/src/core/analytics/index.ts b/airbyte-webapp/src/core/analytics/index.ts new file mode 100644 index 000000000000..79ba376b239c --- /dev/null +++ b/airbyte-webapp/src/core/analytics/index.ts @@ -0,0 +1,2 @@ +export { AnalyticsService } from "./AnalyticsService"; +export { Namespace, Action } from "./types"; diff --git a/airbyte-webapp/src/core/analytics/types.ts b/airbyte-webapp/src/core/analytics/types.ts index 78e5e4fff9df..0b4e8f423171 100644 --- a/airbyte-webapp/src/core/analytics/types.ts +++ b/airbyte-webapp/src/core/analytics/types.ts @@ -1,4 +1,4 @@ -interface SegmentAnalytics { +export interface SegmentAnalytics { page: (name?: string) => void; reset: () => void; alias: (newId: string) => void; @@ -7,4 +7,33 @@ interface SegmentAnalytics { group: (organisationId: string, traits: Record) => void; } -export type { SegmentAnalytics }; +export const enum Namespace { + SOURCE = "Source", + DESTINATION = "Destination", + CONNECTION = "Connection", + CONNECTOR = "Connector", + ONBOARDING = "Onboarding", + USER = "User", +} + +export const enum Action { + CREATE = "Create", + TEST = "Test", + SELECT = "Select", + SUCCESS = "TestSuccess", + FAILURE = "TestFailure", + FREQUENCY = "FrequencySet", + SYNC = "FullRefreshSync", + EDIT_SCHEMA = "EditSchema", + DISABLE = "Disable", + REENABLE = "Reenable", + DELETE = "Delete", + REQUEST = "Request", + SKIP = "Skip", + FEEDBACK = "Feedback", + PREFERENCES = "Preferences", + NO_MATCHING_CONNECTOR = "NoMatchingConnector", + SELECTION_OPENED = "SelectionOpened", +} + +export type EventParams = Record; diff --git a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx index ae2708354b84..1d53ce8de3d7 100644 --- a/airbyte-webapp/src/hooks/services/useConnectionHook.tsx +++ b/airbyte-webapp/src/hooks/services/useConnectionHook.tsx @@ -1,10 +1,10 @@ import { QueryClient, useMutation, useQueryClient } from "react-query"; import { getFrequencyConfig } from "config/utils"; +import { Action, Namespace } from "core/analytics"; import { SyncSchema } from "core/domain/catalog"; import { WebBackendConnectionService } from "core/domain/connection"; import { ConnectionService } from "core/domain/connection/ConnectionService"; -import { TrackActionLegacyType, useTrackAction, TrackActionNamespace, TrackActionType } from "hooks/useTrackAction"; import { useInitService } from "services/useInitService"; import { useConfig } from "../../config"; @@ -21,6 +21,7 @@ import { import { useSuspenseQuery } from "../../services/connector/useSuspenseQuery"; import { SCOPE_WORKSPACE } from "../../services/Scope"; import { useDefaultRequestMiddlewares } from "../../services/useDefaultRequestMiddlewares"; +import { useAnalyticsService } from "./Analytics"; import { useCurrentWorkspace } from "./useWorkspace"; export const connectionsKeys = { @@ -88,15 +89,16 @@ export const useConnectionLoad = ( export const useSyncConnection = () => { const service = useConnectionService(); - const trackSourceAction = useTrackAction(TrackActionNamespace.SOURCE, TrackActionLegacyType.SOURCE); + const analyticsService = useAnalyticsService(); return useMutation((connection: WebBackendConnectionRead) => { const frequency = getFrequencyConfig(connection.schedule); - trackSourceAction("Full refresh sync", TrackActionType.SYNC, { + analyticsService.track(Namespace.CONNECTION, Action.SYNC, { + actionDescription: "Manual triggered sync", connector_source: connection.source?.sourceName, connector_source_definition_id: connection.source?.sourceDefinitionId, - connector_destination: connection.destination?.name, + connector_destination: connection.destination?.destinationName, connector_destination_definition_id: connection.destination?.destinationDefinitionId, frequency: frequency?.type, }); @@ -120,10 +122,7 @@ const useGetConnection = (connectionId: string, options?: { refetchInterval: num const useCreateConnection = () => { const service = useWebConnectionService(); const queryClient = useQueryClient(); - const trackNewConnectionAction = useTrackAction( - TrackActionNamespace.CONNECTION, - TrackActionLegacyType.NEW_CONNECTION - ); + const analyticsService = useAnalyticsService(); return useMutation( async ({ @@ -146,7 +145,8 @@ const useCreateConnection = () => { const frequencyData = getFrequencyConfig(values.schedule); - trackNewConnectionAction("Set up connection", TrackActionType.CREATE, { + analyticsService.track(Namespace.CONNECTION, Action.CREATE, { + actionDescription: "New connection created", frequency: frequencyData?.type || "", connector_source_definition: source?.sourceName, connector_source_definition_id: sourceDefinition?.sourceDefinitionId, diff --git a/airbyte-webapp/src/hooks/services/useDestinationHook.tsx b/airbyte-webapp/src/hooks/services/useDestinationHook.tsx index 4ea2d11e4e75..421555b4fcdc 100644 --- a/airbyte-webapp/src/hooks/services/useDestinationHook.tsx +++ b/airbyte-webapp/src/hooks/services/useDestinationHook.tsx @@ -1,8 +1,8 @@ import { useMutation, useQueryClient } from "react-query"; +import { Action, Namespace } from "core/analytics"; import { ConnectionConfiguration } from "core/domain/connection"; import { DestinationService } from "core/domain/connector/DestinationService"; -import { TrackActionLegacyType, TrackActionType, TrackActionNamespace, useTrackAction } from "hooks/useTrackAction"; import { useInitService } from "services/useInitService"; import { isDefined } from "utils/common"; @@ -11,6 +11,7 @@ import { DestinationRead, WebBackendConnectionRead } from "../../core/request/Ai import { useSuspenseQuery } from "../../services/connector/useSuspenseQuery"; import { SCOPE_WORKSPACE } from "../../services/Scope"; import { useDefaultRequestMiddlewares } from "../../services/useDefaultRequestMiddlewares"; +import { useAnalyticsService } from "./Analytics"; import { connectionsKeys, ListConnection } from "./useConnectionHook"; import { useCurrentWorkspace } from "./useWorkspace"; @@ -92,14 +93,15 @@ const useCreateDestination = () => { const useDeleteDestination = () => { const service = useDestinationService(); const queryClient = useQueryClient(); - const trackDestinationAction = useTrackAction(TrackActionNamespace.DESTINATION, TrackActionLegacyType.SOURCE); + const analyticsService = useAnalyticsService(); return useMutation( (payload: { destination: DestinationRead; connectionsWithDestination: WebBackendConnectionRead[] }) => service.delete(payload.destination.destinationId), { onSuccess: (_data, ctx) => { - trackDestinationAction("Delete destination", TrackActionType.DELETE, { + analyticsService.track(Namespace.DESTINATION, Action.DELETE, { + actionDescription: "Destination deleted", connector_destination: ctx.destination.destinationName, connector_destination_definition_id: ctx.destination.destinationDefinitionId, }); diff --git a/airbyte-webapp/src/hooks/services/useRequestConnector.tsx b/airbyte-webapp/src/hooks/services/useRequestConnector.tsx index c0f5cc5954a8..8122931bb739 100644 --- a/airbyte-webapp/src/hooks/services/useRequestConnector.tsx +++ b/airbyte-webapp/src/hooks/services/useRequestConnector.tsx @@ -1,3 +1,4 @@ +import { Action, Namespace } from "core/analytics"; import { useAnalyticsService } from "hooks/services/Analytics/useAnalyticsService"; interface Values { @@ -13,7 +14,8 @@ const useRequestConnector = (): { const analyticsService = useAnalyticsService(); const requestConnector = (values: Values) => { - analyticsService.track("Request a Connector", { + analyticsService.track(Namespace.CONNECTOR, Action.REQUEST, { + actionDescription: "Request new connector", email: values.email, // This parameter has a legacy name from when it was only the webpage, but we wanted to keep the parameter // name the same after renaming the field to additional information diff --git a/airbyte-webapp/src/hooks/services/useSourceHook.tsx b/airbyte-webapp/src/hooks/services/useSourceHook.tsx index 3bd5d01f4374..e1ff67503a61 100644 --- a/airbyte-webapp/src/hooks/services/useSourceHook.tsx +++ b/airbyte-webapp/src/hooks/services/useSourceHook.tsx @@ -2,11 +2,11 @@ import { useCallback, useEffect, useState } from "react"; import { useMutation, useQueryClient } from "react-query"; import { useConfig } from "config"; +import { Action, Namespace } from "core/analytics"; import { SyncSchema } from "core/domain/catalog"; import { ConnectionConfiguration } from "core/domain/connection"; import { SourceService } from "core/domain/connector/SourceService"; import { JobInfo } from "core/domain/job"; -import { TrackActionLegacyType, TrackActionType, TrackActionNamespace, useTrackAction } from "hooks/useTrackAction"; import { useInitService } from "services/useInitService"; import { isDefined } from "utils/common"; @@ -14,6 +14,7 @@ import { SourceRead, SynchronousJobRead, WebBackendConnectionRead } from "../../ import { useSuspenseQuery } from "../../services/connector/useSuspenseQuery"; import { SCOPE_WORKSPACE } from "../../services/Scope"; import { useDefaultRequestMiddlewares } from "../../services/useDefaultRequestMiddlewares"; +import { useAnalyticsService } from "./Analytics"; import { connectionsKeys, ListConnection } from "./useConnectionHook"; import { useCurrentWorkspace } from "./useWorkspace"; @@ -98,14 +99,15 @@ const useCreateSource = () => { const useDeleteSource = () => { const service = useSourceService(); const queryClient = useQueryClient(); - const trackSourceAction = useTrackAction(TrackActionNamespace.SOURCE, TrackActionLegacyType.SOURCE); + const analyticsService = useAnalyticsService(); return useMutation( (payload: { source: SourceRead; connectionsWithSource: WebBackendConnectionRead[] }) => service.delete(payload.source.sourceId), { onSuccess: (_data, ctx) => { - trackSourceAction("Delete source", TrackActionType.DELETE, { + analyticsService.track(Namespace.SOURCE, Action.DELETE, { + actionDescription: "Source deleted", connector_source: ctx.source.sourceName, connector_source_definition_id: ctx.source.sourceDefinitionId, }); diff --git a/airbyte-webapp/src/hooks/services/useWorkspace.tsx b/airbyte-webapp/src/hooks/services/useWorkspace.tsx index 752503745f4b..ead12b156f56 100644 --- a/airbyte-webapp/src/hooks/services/useWorkspace.tsx +++ b/airbyte-webapp/src/hooks/services/useWorkspace.tsx @@ -1,5 +1,6 @@ import { useMutation } from "react-query"; +import { Action, Namespace } from "core/analytics"; import { NotificationService } from "core/domain/notification/NotificationService"; import { DestinationRead, SourceRead } from "core/request/AirbyteClient"; import { useAnalyticsService } from "hooks/services/Analytics"; @@ -29,11 +30,10 @@ const useWorkspace = () => { const analyticsService = useAnalyticsService(); const finishOnboarding = async (skipStep?: string) => { - if (skipStep) { - analyticsService.track("Skip Onboarding", { - step: skipStep, - }); - } + analyticsService.track(Namespace.ONBOARDING, Action.SKIP, { + actionDescription: "Skip Onboarding", + step: skipStep, + }); await updateWorkspace({ workspaceId: workspace.workspaceId, @@ -54,7 +54,8 @@ const useWorkspace = () => { source: SourceRead; destination: DestinationRead; }) => { - analyticsService.track("Onboarding Feedback", { + analyticsService.track(Namespace.ONBOARDING, Action.FEEDBACK, { + actionDescription: "Onboarding Feedback", feedback, connector_source_definition: source?.sourceName, connector_source_definition_id: source?.sourceDefinitionId, @@ -76,7 +77,8 @@ const useWorkspace = () => { ...data, }); - analyticsService.track("Specified Preferences", { + analyticsService.track(Namespace.ONBOARDING, Action.PREFERENCES, { + actionDescription: "Setup preferences set", email: data.email, anonymized: data.anonymousDataCollection, subscribed_newsletter: data.news, diff --git a/airbyte-webapp/src/hooks/useTrackAction.test.tsx b/airbyte-webapp/src/hooks/useTrackAction.test.tsx deleted file mode 100644 index ed92f36ccac2..000000000000 --- a/airbyte-webapp/src/hooks/useTrackAction.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { renderHook } from "@testing-library/react-hooks"; - -import { useAnalyticsService } from "./services/Analytics/useAnalyticsService"; -import { TrackActionLegacyType, TrackActionNamespace, TrackActionType, useTrackAction } from "./useTrackAction"; - -jest.mock("./services/Analytics/useAnalyticsService", () => { - const mockTrack = jest.fn(); - return { useAnalyticsService: () => ({ track: mockTrack }) }; -}); - -describe("With legacy namespace", () => { - test("it parses namespace and legacy name when calling the hook", () => { - const mockUseTrackAction = renderHook(() => - useTrackAction(TrackActionNamespace.SOURCE, TrackActionLegacyType.NEW_SOURCE) - ); - mockUseTrackAction.result.current("test action sent", TrackActionType.CREATE, {}); - const analyticsService = useAnalyticsService(); - - expect(analyticsService.track).toHaveBeenCalledWith( - "Airbyte.UI.Source.Create", - expect.objectContaining({ legacy_event_name: "New Source - Action" }) - ); - }); -}); - -describe("Without legacy namespace", () => { - test("legacy namespace is passed as empty string if none is received", () => { - const mockUseTrackAction = renderHook(() => useTrackAction(TrackActionNamespace.CONNECTION)); - mockUseTrackAction.result.current("another test action", TrackActionType.CREATE, {}); - const analyticsService = useAnalyticsService(); - expect(analyticsService.track).toHaveBeenCalledWith( - "Airbyte.UI.Connection.Create", - expect.objectContaining({ legacy_event_name: "" }) - ); - }); -}); diff --git a/airbyte-webapp/src/hooks/useTrackAction.ts b/airbyte-webapp/src/hooks/useTrackAction.ts deleted file mode 100644 index 3494a0e9e97d..000000000000 --- a/airbyte-webapp/src/hooks/useTrackAction.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useCallback } from "react"; - -import { useAnalyticsService } from "./services/Analytics/useAnalyticsService"; - -export const enum TrackActionLegacyType { - NEW_SOURCE = "New Source", - NEW_DESTINATION = "New Destination", - NEW_CONNECTION = "New Connection", - SOURCE = "Source", -} - -export const enum TrackActionType { - CREATE = "Create", - TEST = "Test", - SELECT = "Select", - SUCCESS = "TestSuccess", - FAILURE = "TestFailure", - FREQUENCY = "FrequencySet", - SYNC = "FullRefreshSync", - SCHEMA = "EditSchema", - DISABLE = "Disable", - REENABLE = "Reenable", - DELETE = "Delete", -} - -export const enum TrackActionNamespace { - SOURCE = "Source", - DESTINATION = "Destination", - CONNECTION = "Connection", -} - -interface TrackConnectorActionProperties { - connector_source?: string; - connector_source_definition_id?: string; - connector_destination?: string; - connector_destination_definition_id?: string; -} - -interface TrackConnectionActionProperties { - frequency: string; - connector_source_definition: string; - connector_source_definition_id: string; - connector_destination_definition: string; - connector_destination_definition_id: string; - available_streams: number; - enabled_streams: number; -} - -export const useTrackAction = (namespace: TrackActionNamespace, legacyType?: TrackActionLegacyType) => { - const analyticsService = useAnalyticsService(); - - return useCallback( - ( - actionDescription: string, - actionType: TrackActionType, - properties: TrackConnectorActionProperties | TrackConnectionActionProperties - ) => { - // Calls that did not exist in the legacy format will not have a legacy event name - const legacyEventName = legacyType ? `${legacyType} - Action` : ""; - - analyticsService.track(`Airbyte.UI.${namespace}.${actionType}`, { - actionDescription, - ...properties, - legacy_event_name: legacyEventName, - }); - }, - [analyticsService, namespace, legacyType] - ); -}; diff --git a/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx b/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx index 695fba9d3874..70a32f4f980f 100644 --- a/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx +++ b/airbyte-webapp/src/packages/cloud/services/auth/AuthService.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useContext, useMemo, useRef } from "react"; import { useQueryClient } from "react-query"; import { useEffectOnce } from "react-use"; +import { Action, Namespace } from "core/analytics"; import { useAnalyticsService } from "hooks/services/Analytics"; import useTypesafeReducer from "hooks/useTypesafeReducer"; import { AuthProviders } from "packages/cloud/lib/auth/AuthProviders"; @@ -179,7 +180,8 @@ export const AuthenticationProvider: React.FC = ({ children }) => { // Send verification mail via firebase await authService.sendEmailVerifiedLink(); - analytics.track("Airbyte.UI.User.Created", { + analytics.track(Namespace.USER, Action.CREATE, { + actionDescription: "New user registered", user_id: fbUser.uid, name: user.name, email: user.email, diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx index b4cedc15fcb7..790f41f8cd7f 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/ConnectionItemPage.tsx @@ -5,9 +5,10 @@ import { LoadingPage, MainPageWithScroll } from "components"; import HeadTitle from "components/HeadTitle"; import { getFrequencyConfig } from "config/utils"; +import { Action, Namespace } from "core/analytics"; import { ConnectionStatus } from "core/request/AirbyteClient"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { useGetConnection } from "hooks/services/useConnectionHook"; -import { TrackActionLegacyType, TrackActionType, TrackActionNamespace, useTrackAction } from "hooks/useTrackAction"; import TransformationView from "pages/ConnectionPage/pages/ConnectionItemPage/components/TransformationView"; import ConnectionPageTitle from "./components/ConnectionPageTitle"; @@ -25,15 +26,15 @@ const ConnectionItemPage: React.FC = () => { const currentStep = params["*"] || ConnectionSettingsRoutes.STATUS; const connection = useGetConnection(connectionId); const [isStatusUpdating, setStatusUpdating] = useState(false); + const analyticsService = useAnalyticsService(); const { source, destination } = connection; const frequency = getFrequencyConfig(connection.schedule); - const trackSourceAction = useTrackAction(TrackActionNamespace.SOURCE, TrackActionLegacyType.SOURCE); - const onAfterSaveSchema = () => { - trackSourceAction("Edit schema", TrackActionType.SCHEMA, { + analyticsService.track(Namespace.CONNECTION, Action.EDIT_SCHEMA, { + actionDescription: "Connection saved with catalog changes", connector_source: source.sourceName, connector_source_definition_id: source.sourceDefinitionId, connector_destination: destination.destinationName, diff --git a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx index a8e4869259cf..2b16ead1997b 100644 --- a/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx +++ b/airbyte-webapp/src/pages/ConnectionPage/pages/ConnectionItemPage/components/EnabledControl.tsx @@ -5,9 +5,10 @@ import styled from "styled-components"; import { Switch } from "components"; +import { Action, Namespace } from "core/analytics"; import { buildConnectionUpdate } from "core/domain/connection"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { useUpdateConnection } from "hooks/services/useConnectionHook"; -import { TrackActionLegacyType, TrackActionType, TrackActionNamespace, useTrackAction } from "hooks/useTrackAction"; import { ConnectionStatus, WebBackendConnectionRead } from "../../../../../core/request/AirbyteClient"; @@ -37,7 +38,7 @@ interface EnabledControlProps { const EnabledControl: React.FC = ({ connection, disabled, frequencyType, onStatusUpdating }) => { const { mutateAsync: updateConnection, isLoading } = useUpdateConnection(); - const trackSourceAction = useTrackAction(TrackActionNamespace.SOURCE, TrackActionLegacyType.SOURCE); + const analyticsService = useAnalyticsService(); const onChangeStatus = async () => { await updateConnection( @@ -46,15 +47,13 @@ const EnabledControl: React.FC = ({ connection, disabled, f }) ); - const trackableAction = - connection.status === ConnectionStatus.active ? TrackActionType.DISABLE : TrackActionType.REENABLE; + const trackableAction = connection.status === ConnectionStatus.active ? Action.DISABLE : Action.REENABLE; - const trackableActionString = `${trackableAction} connection`; - - trackSourceAction(trackableActionString, trackableAction, { + analyticsService.track(Namespace.CONNECTION, trackableAction, { + actionDescription: `${trackableAction} connection`, connector_source: connection.source?.sourceName, connector_source_definition_id: connection.source?.sourceDefinitionId, - connector_destination: connection.destination?.name, + connector_destination: connection.destination?.destinationName, connector_destination_definition_id: connection.destination?.destinationDefinitionId, frequency: frequencyType, }); diff --git a/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx b/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx index 495ff27c0f90..a31baae84278 100644 --- a/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx +++ b/airbyte-webapp/src/pages/DestinationPage/pages/CreateDestinationPage/components/DestinationForm.tsx @@ -1,11 +1,12 @@ import React, { useState } from "react"; import { FormattedMessage } from "react-intl"; +import { Action, Namespace } from "core/analytics"; import { ConnectionConfiguration } from "core/domain/connection"; import { DestinationDefinitionRead } from "core/request/AirbyteClient"; import { LogsRequestError } from "core/request/LogsRequestError"; +import { useAnalyticsService } from "hooks/services/Analytics"; import useRouter from "hooks/useRouter"; -import { TrackActionLegacyType, TrackActionType, TrackActionNamespace, useTrackAction } from "hooks/useTrackAction"; import { useGetDestinationDefinitionSpecificationAsync } from "services/connector/DestinationDefinitionSpecificationService"; import { createFormErrorMessage } from "utils/errorStatusMessage"; import { ConnectorCard } from "views/Connector/ConnectorCard"; @@ -39,10 +40,7 @@ export const DestinationForm: React.FC = ({ afterSelectConnector, }) => { const { location } = useRouter(); - const trackNewDestinationAction = useTrackAction( - TrackActionNamespace.DESTINATION, - TrackActionLegacyType.NEW_DESTINATION - ); + const analyticsService = useAnalyticsService(); const [destinationDefinitionId, setDestinationDefinitionId] = useState( hasDestinationDefinitionId(location.state) ? location.state.destinationDefinitionId : null @@ -63,7 +61,8 @@ export const DestinationForm: React.FC = ({ afterSelectConnector(); } - trackNewDestinationAction("Select a connector", TrackActionType.SELECT, { + analyticsService.track(Namespace.DESTINATION, Action.SELECT, { + actionDescription: "Destination connector type selected", connector_destination: connector?.name, connector_destination_definition_id: destinationDefinitionId, }); diff --git a/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx b/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx index 536ed36041aa..be82a8631f74 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/OnboardingPage.tsx @@ -87,7 +87,7 @@ const OnboardingPage: React.FC = () => { ); const handleFinishOnboarding = () => { - finishOnboarding(); + finishOnboarding(currentStep); push(RoutePaths.Connections); }; diff --git a/airbyte-webapp/src/pages/OnboardingPage/components/DestinationStep.tsx b/airbyte-webapp/src/pages/OnboardingPage/components/DestinationStep.tsx index 10ee431b2433..a910a377df0e 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/components/DestinationStep.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/components/DestinationStep.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from "react"; +import { Action, Namespace } from "core/analytics"; import { ConnectionConfiguration } from "core/domain/connection"; import { JobInfo } from "core/domain/job"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { useCreateDestination } from "hooks/services/useDestinationHook"; -import { TrackActionLegacyType, TrackActionType, TrackActionNamespace, useTrackAction } from "hooks/useTrackAction"; import { useDestinationDefinitionList } from "services/connector/DestinationDefinitionService"; import { useGetDestinationDefinitionSpecificationAsync } from "services/connector/DestinationDefinitionSpecificationService"; import { createFormErrorMessage } from "utils/errorStatusMessage"; @@ -30,10 +31,7 @@ const DestinationStep: React.FC = ({ onNextStep, onSuccess }) => { const { mutateAsync: createDestination } = useCreateDestination(); - const trackNewDestinationAction = useTrackAction( - TrackActionNamespace.DESTINATION, - TrackActionLegacyType.NEW_DESTINATION - ); + const analyticsService = useAnalyticsService(); const getDestinationDefinitionById = (id: string) => destinationDefinitions.find((item) => item.destinationDefinitionId === id); @@ -75,7 +73,8 @@ const DestinationStep: React.FC = ({ onNextStep, onSuccess }) => { const destinationConnector = getDestinationDefinitionById(destinationDefinitionId); setDocumentationUrl(destinationConnector?.documentationUrl || ""); - trackNewDestinationAction("Select a connector", TrackActionType.SELECT, { + analyticsService.track(Namespace.DESTINATION, Action.SELECT, { + actionDescription: "Destination connector type selected", connector_destination: destinationConnector?.name, connector_destination_definition_id: destinationConnector?.destinationDefinitionId, }); diff --git a/airbyte-webapp/src/pages/OnboardingPage/components/SkipOnboardingButton.tsx b/airbyte-webapp/src/pages/OnboardingPage/components/SkipOnboardingButton.tsx deleted file mode 100644 index b6df07ae5dd2..000000000000 --- a/airbyte-webapp/src/pages/OnboardingPage/components/SkipOnboardingButton.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from "react"; -import { FormattedMessage } from "react-intl"; -import styled from "styled-components"; - -import { Button } from "components"; - -import useWorkspace from "hooks/services/useWorkspace"; - -const ButtonWithMargin = styled(Button)` - margin-right: 9px; -`; - -interface IProps { - step: string; -} - -const SkipOnboardingButton: React.FC = ({ step }) => { - const { finishOnboarding } = useWorkspace(); - - const onSkip = async () => { - await finishOnboarding(step); - }; - - return ( - - - - ); -}; - -export default SkipOnboardingButton; diff --git a/airbyte-webapp/src/pages/OnboardingPage/components/SourceStep.tsx b/airbyte-webapp/src/pages/OnboardingPage/components/SourceStep.tsx index 43b80831388d..2aea7372f02f 100644 --- a/airbyte-webapp/src/pages/OnboardingPage/components/SourceStep.tsx +++ b/airbyte-webapp/src/pages/OnboardingPage/components/SourceStep.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useState } from "react"; +import { Action, Namespace } from "core/analytics"; import { ConnectionConfiguration } from "core/domain/connection"; import { JobInfo } from "core/domain/job"; import { LogsRequestError } from "core/request/LogsRequestError"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { useCreateSource } from "hooks/services/useSourceHook"; -import { TrackActionLegacyType, TrackActionType, TrackActionNamespace, useTrackAction } from "hooks/useTrackAction"; import { useSourceDefinitionList } from "services/connector/SourceDefinitionService"; import { useGetSourceDefinitionSpecificationAsync } from "services/connector/SourceDefinitionSpecificationService"; import { createFormErrorMessage } from "utils/errorStatusMessage"; @@ -29,7 +30,7 @@ const SourceStep: React.FC = ({ onNextStep, onSuccess }) => { const { setDocumentationUrl, setDocumentationPanelOpen } = useDocumentationPanelContext(); const { mutateAsync: createSource } = useCreateSource(); - const trackNewSourceAction = useTrackAction(TrackActionNamespace.SOURCE, TrackActionLegacyType.NEW_SOURCE); + const analyticsService = useAnalyticsService(); const getSourceDefinitionById = (id: string) => sourceDefinitions.find((item) => item.sourceDefinitionId === id); @@ -75,7 +76,8 @@ const SourceStep: React.FC = ({ onNextStep, onSuccess }) => { const sourceDefinition = getSourceDefinitionById(sourceId); setDocumentationUrl(sourceDefinition?.documentationUrl || ""); - trackNewSourceAction("Select a connector", TrackActionType.SELECT, { + analyticsService.track(Namespace.SOURCE, Action.SELECT, { + actionDescription: "Source connector type selected", connector_source: sourceDefinition?.name, connector_source_definition_id: sourceDefinition?.sourceDefinitionId, }); diff --git a/airbyte-webapp/src/pages/SourcesPage/pages/CreateSourcePage/components/SourceForm.tsx b/airbyte-webapp/src/pages/SourcesPage/pages/CreateSourcePage/components/SourceForm.tsx index 4d60cc04cff1..e2ee9c1251b5 100644 --- a/airbyte-webapp/src/pages/SourcesPage/pages/CreateSourcePage/components/SourceForm.tsx +++ b/airbyte-webapp/src/pages/SourcesPage/pages/CreateSourcePage/components/SourceForm.tsx @@ -1,10 +1,11 @@ import React, { useState } from "react"; import { FormattedMessage } from "react-intl"; +import { Action, Namespace } from "core/analytics"; import { ConnectionConfiguration } from "core/domain/connection"; import { LogsRequestError } from "core/request/LogsRequestError"; +import { useAnalyticsService } from "hooks/services/Analytics"; import useRouter from "hooks/useRouter"; -import { TrackActionLegacyType, TrackActionType, TrackActionNamespace, useTrackAction } from "hooks/useTrackAction"; import { SourceDefinitionReadWithLatestTag } from "services/connector/SourceDefinitionService"; import { useGetSourceDefinitionSpecificationAsync } from "services/connector/SourceDefinitionSpecificationService"; import { createFormErrorMessage } from "utils/errorStatusMessage"; @@ -40,7 +41,7 @@ export const SourceForm: React.FC = ({ afterSelectConnector, }) => { const { location } = useRouter(); - const trackNewSourceAction = useTrackAction(TrackActionNamespace.SOURCE, TrackActionLegacyType.NEW_SOURCE); + const analyticsService = useAnalyticsService(); const [sourceDefinitionId, setSourceDefinitionId] = useState( hasSourceDefinitionId(location.state) ? location.state.sourceDefinitionId : null @@ -60,7 +61,9 @@ export const SourceForm: React.FC = ({ if (afterSelectConnector) { afterSelectConnector(); } - trackNewSourceAction("Select a connector", TrackActionType.SELECT, { + + analyticsService.track(Namespace.SOURCE, Action.SELECT, { + actionDescription: "Source connector type selected", connector_source: connector?.name, connector_source_definition_id: sourceDefinitionId, }); diff --git a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx index 684198071b56..e9c485328189 100644 --- a/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx +++ b/airbyte-webapp/src/views/Connector/ConnectorCard/ConnectorCard.tsx @@ -4,10 +4,11 @@ import { FormattedMessage } from "react-intl"; import { ContentCard } from "components"; import { JobItem } from "components/JobItem/JobItem"; +import { Action, Namespace } from "core/analytics"; import { Connector, ConnectorT } from "core/domain/connector"; import { CheckConnectionRead } from "core/request/AirbyteClient"; import { LogsRequestError, SynchronousJobReadWithStatus } from "core/request/LogsRequestError"; -import { TrackActionLegacyType, TrackActionType, TrackActionNamespace, useTrackAction } from "hooks/useTrackAction"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { createFormErrorMessage } from "utils/errorStatusMessage"; import { ServiceForm, ServiceFormProps, ServiceFormValues } from "views/Connector/ServiceForm"; @@ -45,42 +46,34 @@ export const ConnectorCard: React.FC< setErrorStatusRequest(null); }, [props.selectedConnectorDefinitionSpecification, reset]); - const trackNewSourceAction = useTrackAction(TrackActionNamespace.SOURCE, TrackActionLegacyType.NEW_SOURCE); - const trackNewDestinationAction = useTrackAction( - TrackActionNamespace.DESTINATION, - TrackActionLegacyType.NEW_DESTINATION - ); + const analyticsService = useAnalyticsService(); const onHandleSubmit = async (values: ServiceFormValues) => { setErrorStatusRequest(null); const connector = props.availableServices.find((item) => Connector.id(item) === values.serviceType); - const trackAction = (action: string, actionType: TrackActionType) => { + const trackAction = (actionType: Action, actionDescription: string) => { if (!connector) { return; } - if (props.formType === "source") { - trackNewSourceAction(action, actionType, { - connector_source: connector?.name, - connector_source_definition_id: Connector.id(connector), - }); - } else { - trackNewDestinationAction(action, actionType, { - connector_destination: connector?.name, - connector_destination_definition_id: Connector.id(connector), - }); - } + const namespace = props.formType === "source" ? Namespace.SOURCE : Namespace.DESTINATION; + + analyticsService.track(namespace, actionType, { + actionDescription, + connector_source: connector?.name, + connector_source_definition_id: Connector.id(connector), + }); }; const testConnectorWithTracking = async () => { - trackAction("Test a connector", TrackActionType.TEST); + trackAction(Action.TEST, "Test a connector"); try { await testConnector(values); - trackAction("Tested connector - success", TrackActionType.SUCCESS); + trackAction(Action.SUCCESS, "Tested connector - success"); } catch (e) { - trackAction("Tested connector - failure", TrackActionType.FAILURE); + trackAction(Action.FAILURE, "Tested connector - failure"); throw e; } }; diff --git a/airbyte-webapp/src/views/Connector/ServiceForm/components/Controls/ConnectorServiceTypeControl.tsx b/airbyte-webapp/src/views/Connector/ServiceForm/components/Controls/ConnectorServiceTypeControl.tsx index 9ba80a5edf6d..d685e333368d 100644 --- a/airbyte-webapp/src/views/Connector/ServiceForm/components/Controls/ConnectorServiceTypeControl.tsx +++ b/airbyte-webapp/src/views/Connector/ServiceForm/components/Controls/ConnectorServiceTypeControl.tsx @@ -15,6 +15,7 @@ import { import { ConnectorIcon } from "components/ConnectorIcon"; import { GAIcon } from "components/icons/GAIcon"; +import { Action, Namespace } from "core/analytics"; import { Connector, ConnectorDefinition } from "core/domain/connector"; import { FormBaseItem } from "core/form/types"; import { ReleaseStage } from "core/request/AirbyteClient"; @@ -209,14 +210,10 @@ const ConnectorServiceTypeControl: React.FC = const getNoOptionsMessage = useCallback( ({ inputValue }: { inputValue: string }) => { - analytics.track( - formType === "source" - ? "Airbyte.UI.NewSource.NoMatchingConnector" - : "Airbyte.UI.NewDestination.NoMatchingConnector", - { - query: inputValue, - } - ); + analytics.track(formType === "source" ? Namespace.SOURCE : Namespace.DESTINATION, Action.NO_MATCHING_CONNECTOR, { + actionDescription: "Connector query without results", + query: inputValue, + }); return formatMessage({ id: "form.noConnectorFound" }); }, [analytics, formType, formatMessage] @@ -240,9 +237,9 @@ const ConnectorServiceTypeControl: React.FC = ); const onMenuOpen = () => { - const eventName = - formType === "source" ? "Airbyte.UI.NewSource.SelectionOpened" : "Airbyte.UI.NewDestination.SelectionOpened"; - analytics.track(eventName, {}); + analytics.track(formType === "source" ? Namespace.SOURCE : Namespace.DESTINATION, Action.SELECTION_OPENED, { + actionDescription: "Opened connector type selection", + }); }; return (