Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AnalyticsService event cleanup #15142

Merged
merged 5 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -40,10 +41,7 @@ const CreateConnectionContent: React.FC<CreateConnectionContentProps> = ({
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);

Expand Down Expand Up @@ -90,7 +88,8 @@ const CreateConnectionContent: React.FC<CreateConnectionContentProps> = ({
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,
Expand Down
14 changes: 6 additions & 8 deletions airbyte-webapp/src/components/EntityTable/hooks.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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(
Expand All @@ -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,
Expand Down
57 changes: 57 additions & 0 deletions airbyte-webapp/src/core/analytics/AnalyticsService.test.ts
Original file line number Diff line number Diff line change
@@ -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 })
);
});
});
9 changes: 5 additions & 4 deletions airbyte-webapp/src/core/analytics/AnalyticsService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SegmentAnalytics } from "./types";
import { Action, EventParams, Namespace, SegmentAnalytics } from "./types";

export class AnalyticsService {
constructor(private context: Record<string, unknown>, private version?: string) {}
Expand All @@ -11,13 +11,14 @@ export class AnalyticsService {

reset = (): void => this.getSegmentAnalytics()?.reset?.();

track = <P = Record<string, unknown>>(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<string, unknown> = {}): void => {
this.getSegmentAnalytics()?.identify?.(userId, traits);
Expand Down
2 changes: 2 additions & 0 deletions airbyte-webapp/src/core/analytics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AnalyticsService } from "./AnalyticsService";
export { Namespace, Action } from "./types";
33 changes: 31 additions & 2 deletions airbyte-webapp/src/core/analytics/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
interface SegmentAnalytics {
export interface SegmentAnalytics {
page: (name?: string) => void;
reset: () => void;
alias: (newId: string) => void;
Expand All @@ -7,4 +7,33 @@ interface SegmentAnalytics {
group: (organisationId: string, traits: Record<string, unknown>) => 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<string, unknown>;
18 changes: 9 additions & 9 deletions airbyte-webapp/src/hooks/services/useConnectionHook.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 = {
Expand Down Expand Up @@ -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,
});
Expand All @@ -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 ({
Expand All @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions airbyte-webapp/src/hooks/services/useDestinationHook.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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";

Expand Down Expand Up @@ -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,
});
Expand Down
4 changes: 3 additions & 1 deletion airbyte-webapp/src/hooks/services/useRequestConnector.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Action, Namespace } from "core/analytics";
import { useAnalyticsService } from "hooks/services/Analytics/useAnalyticsService";

interface Values {
Expand All @@ -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
Expand Down
8 changes: 5 additions & 3 deletions airbyte-webapp/src/hooks/services/useSourceHook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@ 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";

import { SourceRead, SynchronousJobRead, WebBackendConnectionRead } from "../../core/request/AirbyteClient";
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";

Expand Down Expand Up @@ -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,
});
Expand Down
16 changes: 9 additions & 7 deletions airbyte-webapp/src/hooks/services/useWorkspace.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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, {
Copy link
Contributor

@dizel852 dizel852 Aug 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I see previously this action was performed conditionally(is skipStep provided)
Was the condition removed intentionally?
It can be "step: undefined" potentially.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that was on purpose. This if actually prevented the event from firing, since we never passed it in. I now passed the currentStep into the call, but even if that wouldn't exist, I think we want to send this event out, thus I removed the if completely.

actionDescription: "Skip Onboarding",
step: skipStep,
});

await updateWorkspace({
workspaceId: workspace.workspaceId,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Loading