diff --git a/airbyte-webapp-e2e-tests/cypress/integration/connectorBuilder.spec.ts b/airbyte-webapp-e2e-tests/cypress/integration/connectorBuilder.spec.ts index 059e317cfbf89..5377bb69abe7e 100644 --- a/airbyte-webapp-e2e-tests/cypress/integration/connectorBuilder.spec.ts +++ b/airbyte-webapp-e2e-tests/cypress/integration/connectorBuilder.spec.ts @@ -1,8 +1,18 @@ import { goToConnectorBuilderPage, testStream } from "pages/connectorBuilderPage"; -import { assertTestReadItems, assertTestReadAuthFailure, configureAuth, configureGlobals, configureStream, configurePagination, assertMultiPageReadItems } from "commands/connectorBuilder"; +import { + assertTestReadItems, + assertTestReadAuthFailure, + configureAuth, + configureGlobals, + configureStream, + configurePagination, + assertMultiPageReadItems, +} from "commands/connectorBuilder"; +import { initialSetupCompleted } from "commands/workspaces"; describe("Connector builder", () => { before(() => { + initialSetupCompleted(); goToConnectorBuilderPage(); }); diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/AddStreamButton.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/AddStreamButton.tsx index 5c6d685997bfd..ab9f46e53d85c 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/AddStreamButton.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/AddStreamButton.tsx @@ -9,7 +9,9 @@ import * as yup from "yup"; import { Button } from "components/ui/Button"; import { Modal, ModalBody, ModalFooter } from "components/ui/Modal"; +import { Action, Namespace } from "core/analytics"; import { FormikPatch } from "core/form/FormikPatch"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { ReactComponent as PlusIcon } from "../../connection/ConnectionOnboarding/plusIcon.svg"; import { BuilderStream, DEFAULT_BUILDER_STREAM_VALUES } from "../types"; @@ -34,6 +36,7 @@ export const AddStreamButton: React.FC = ({ initialValues, "data-testid": testId, }) => { + const analyticsService = useAnalyticsService(); const { formatMessage } = useIntl(); const [isOpen, setIsOpen] = useState(false); const [streamsField, , helpers] = useField("streams"); @@ -57,17 +60,35 @@ export const AddStreamButton: React.FC = ({ { + const id = uuid(); helpers.setValue([ ...streamsField.value, merge({}, DEFAULT_BUILDER_STREAM_VALUES, { ...initialValues, name: values.streamName, urlPath: values.urlPath, - id: uuid(), + id, }), ]); setIsOpen(false); onAddStream(numStreams); + if (initialValues) { + analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.STREAM_COPY, { + actionDescription: "Existing stream copied into a new stream", + existing_stream_id: initialValues.id, + existing_stream_name: initialValues.name, + new_stream_id: id, + new_stream_name: values.streamName, + new_stream_url_path: values.urlPath, + }); + } else { + analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.STREAM_CREATE, { + actionDescription: "New stream created from the Add Stream button", + stream_id: id, + stream_name: values.streamName, + url_path: values.urlPath, + }); + } }} validationSchema={yup.object().shape({ streamName: yup.string().required("form.empty.error"), diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx index 5abfe0d0a85d6..a846658635bb2 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx @@ -1,3 +1,6 @@ +import { Action, Namespace } from "core/analytics"; +import { useAnalyticsService } from "hooks/services/Analytics"; + import { inferredAuthValues } from "../types"; import { BuilderCard } from "./BuilderCard"; import { BuilderField } from "./BuilderField"; @@ -7,12 +10,20 @@ import { BuilderOptional } from "./BuilderOptional"; import { KeyValueListField } from "./KeyValueListField"; export const AuthenticationSection: React.FC = () => { + const analyticsService = useAnalyticsService(); + return ( + analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.AUTHENTICATION_METHOD_SELECT, { + actionDescription: "Authentication method selected", + auth_type: type, + }) + } options={[ { label: "No Auth", typeValue: "NoAuth" }, { diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx index 497523e3a9b09..ae7bf7fa1909b 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderField.tsx @@ -30,7 +30,7 @@ interface BaseFieldProps { // path to the location in the Connector Manifest schema which should be set by this component path: string; label: string; - tooltip?: string; + tooltip?: React.ReactNode; readOnly?: boolean; optional?: boolean; pattern?: RegExp; @@ -40,7 +40,7 @@ interface BaseFieldProps { export type BuilderFieldProps = BaseFieldProps & ( - | { type: "string" | "number" | "integer"; onChange?: (newValue: string) => void } + | { type: "string" | "number" | "integer"; onChange?: (newValue: string) => void; onBlur?: (value: string) => void } | { type: "boolean"; onChange?: (newValue: boolean) => void } | { type: "array"; onChange?: (newValue: string[]) => void } | { type: "enum"; onChange?: (newValue: string) => void; options: string[] } @@ -116,6 +116,9 @@ const InnerBuilderField: React.FC> = error={hasError} readOnly={readOnly} adornment={adornment} + onBlur={(e) => { + props.onBlur?.(e.target.value); + }} /> )} {props.type === "array" && ( diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOneOf.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOneOf.tsx index d8046b0e4c974..9204cff3866ec 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOneOf.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOneOf.tsx @@ -23,6 +23,7 @@ interface BuilderOneOfProps { path: string; // path to the oneOf component in the json schema label: string; tooltip: string; + onSelect?: (type: string) => void; } const InnerBuilderOneOf: React.FC> = ({ @@ -32,6 +33,7 @@ const InnerBuilderOneOf: React.FC> = field: typePathField, path, form, + onSelect, }) => { const value = typePathField.value; @@ -56,6 +58,8 @@ const InnerBuilderOneOf: React.FC> = type: selectedOption.value, ...selectedOption.default, }); + + onSelect?.(selectedOption.value); }} /> } diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderSidebar.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderSidebar.tsx index 2ee753a208be6..b35661eedfef7 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderSidebar.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderSidebar.tsx @@ -10,6 +10,8 @@ import { Button } from "components/ui/Button"; import { Heading } from "components/ui/Heading"; import { Text } from "components/ui/Text"; +import { Action, Namespace } from "core/analytics"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; import { BuilderView, useConnectorBuilderFormState } from "services/connectorBuilder/ConnectorBuilderStateService"; @@ -57,6 +59,7 @@ interface BuilderSidebarProps { } export const BuilderSidebar: React.FC = React.memo(({ className, toggleYamlEditor }) => { + const analyticsService = useAnalyticsService(); const { formatMessage } = useIntl(); const { hasErrors } = useBuilderErrors(); const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); @@ -71,6 +74,9 @@ export const BuilderSidebar: React.FC = React.memo(({ class setValues(DEFAULT_BUILDER_FORM_VALUES); setSelectedView("global"); closeConfirmationModal(); + analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.RESET_ALL, { + actionDescription: "Connector Builder UI reset back to blank slate", + }); }, }); }; @@ -100,7 +106,12 @@ export const BuilderSidebar: React.FC = React.memo(({ class className={styles.globalConfigButton} selected={selectedView === "global"} showErrorIndicator={hasErrors(true, ["global"])} - onClick={() => handleViewSelect("global")} + onClick={() => { + handleViewSelect("global"); + analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.GLOBAL_CONFIGURATION_SELECT, { + actionDescription: "Global Configuration view selected", + }); + }} > @@ -111,7 +122,12 @@ export const BuilderSidebar: React.FC = React.memo(({ class showErrorIndicator={false} className={styles.globalConfigButton} selected={selectedView === "inputs"} - onClick={() => handleViewSelect("inputs")} + onClick={() => { + handleViewSelect("inputs"); + analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.USER_INPUTS_SELECT, { + actionDescription: "User Inputs view selected", + }); + }} > = React.memo(({ class
- {values.streams.map(({ name }, num) => ( + {values.streams.map(({ name, id }, num) => ( handleViewSelect(num)} + onClick={() => { + handleViewSelect(num); + analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.STREAM_SELECT, { + actionDescription: "Stream view selected", + stream_id: id, + stream_name: name, + }); + }} > {name && name.trim() ? ( {name} diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/GlobalConfigView.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/GlobalConfigView.tsx index bc8e29a532a55..72a89aed91d9f 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/GlobalConfigView.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/GlobalConfigView.tsx @@ -1,5 +1,10 @@ import { useIntl } from "react-intl"; +import { TextWithHTML } from "components/ui/TextWithHTML"; + +import { Action, Namespace } from "core/analytics"; +import { useAnalyticsService } from "hooks/services/Analytics"; + import { AuthenticationSection } from "./AuthenticationSection"; import { BuilderCard } from "./BuilderCard"; import { BuilderConfigView } from "./BuilderConfigView"; @@ -9,13 +14,29 @@ import styles from "./GlobalConfigView.module.scss"; export const GlobalConfigView: React.FC = () => { const { formatMessage } = useIntl(); + const analyticsService = useAnalyticsService(); return ( {/* Not using intl for the labels and tooltips in this component in order to keep maintainence simple */} - + + } + onBlur={(value: string) => { + if (value) { + analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.API_URL_CREATE, { + actionDescription: "Base API URL filled in", + api_url: value, + }); + } + }} + /> diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/InputsForm.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/InputsForm.tsx index 2ab7c5415aa77..af5173424d138 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/InputsForm.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/InputsForm.tsx @@ -1,5 +1,4 @@ import { Form, Formik, useField, useFormikContext } from "formik"; -import { JSONSchema7 } from "json-schema"; import { useMemo } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useEffectOnce } from "react-use"; @@ -9,7 +8,10 @@ import { Button } from "components/ui/Button"; import { Callout } from "components/ui/Callout"; import { Modal, ModalBody, ModalFooter } from "components/ui/Modal"; +import { Action, Namespace } from "core/analytics"; import { FormikPatch } from "core/form/FormikPatch"; +import { AirbyteJSONSchema } from "core/jsonSchema/types"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { BuilderFormInput, BuilderFormValues, getInferredInputs } from "../types"; import { BuilderField } from "./BuilderField"; @@ -19,7 +21,7 @@ const supportedTypes = ["string", "integer", "number", "array", "boolean", "enum export interface InputInEditing { key: string; - definition: JSONSchema7; + definition: AirbyteJSONSchema; required: boolean; isNew?: boolean; showDefaultValueField: boolean; @@ -68,6 +70,7 @@ export const InputForm = ({ inputInEditing: InputInEditing; onClose: (newInput?: BuilderFormInput) => void; }) => { + const analyticsService = useAnalyticsService(); const { values, setFieldValue } = useFormikContext(); const [inputs, , helpers] = useField("inputs"); const inferredInputs = useMemo( @@ -112,6 +115,22 @@ export const InputForm = ({ ); onClose(newInput); } + analyticsService.track( + Namespace.CONNECTOR_BUILDER, + values.isNew ? Action.USER_INPUT_CREATE : Action.USER_INPUT_EDIT, + { + actionDescription: values.isNew ? "New user input created" : "Existing user input edited", + user_input_id: values.key, + user_input_name: values.definition.title, + hint: values.definition.description, + type: values.type, + allowed_enum_values: values.definition.enum, + secret_field: values.definition.airbyte_secret, + required_field: values.definition.required, + enable_default_value: values.showDefaultValueField, + default_value: values.definition.default, + } + ); }} > <> @@ -121,6 +140,11 @@ export const InputForm = ({ onDelete={() => { helpers.setValue(inputs.value.filter((input) => input.key !== inputInEditing.key)); onClose(); + analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.USER_INPUT_DELETE, { + actionDescription: "User input deleted", + user_input_id: inputInEditing.key, + user_input_name: inputInEditing.definition.title, + }); }} onClose={() => { onClose(); diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx index 9fa3cf43ff112..270c9b4c5acf3 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/StreamConfigView.tsx @@ -10,7 +10,10 @@ import Indicator from "components/Indicator"; import { Button } from "components/ui/Button"; import { CodeEditor } from "components/ui/CodeEditor"; import { Text } from "components/ui/Text"; +import { TextWithHTML } from "components/ui/TextWithHTML"; +import { Action, Namespace } from "core/analytics"; +import { useAnalyticsService } from "hooks/services/Analytics"; import { useConfirmationModalService } from "hooks/services/ConfirmationModal"; import { BuilderView, @@ -64,7 +67,9 @@ export const StreamConfigView: React.FC = React.memo(({ s type="string" path={streamFieldPath("urlPath")} label="Path URL" - tooltip="Path of the endpoint that this stream represents." + tooltip={ + + } /> void; selectedTab: "configuration" | "schema"; }) => { + const analyticsService = useAnalyticsService(); const { formatMessage } = useIntl(); const [field, , helpers] = useField("streams"); const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService(); @@ -151,6 +157,11 @@ const StreamControls = ({ helpers.setValue(updatedStreams); setSelectedView(viewToSelect); closeConfirmationModal(); + analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.STREAM_DELETE, { + actionDescription: "New stream created from the Add Stream button", + stream_id: field.value[streamNum].id, + stream_name: field.value[streamNum].name, + }); }, }); }; @@ -207,8 +218,9 @@ const StreamTab = ({ ); const SchemaEditor = ({ streamFieldPath }: { streamFieldPath: (fieldPath: string) => string }) => { + const analyticsService = useAnalyticsService(); const [field, meta, helpers] = useField(streamFieldPath("schema")); - const { streamRead } = useConnectorBuilderTestState(); + const { streamRead, streams, testStreamIndex } = useConnectorBuilderTestState(); const showImportButton = !field.value && streamRead.data?.inferred_schema; @@ -219,7 +231,12 @@ const SchemaEditor = ({ streamFieldPath }: { streamFieldPath: (fieldPath: string full variant="secondary" onClick={() => { - helpers.setValue(formatJson(streamRead.data?.inferred_schema, true)); + const formattedJson = formatJson(streamRead.data?.inferred_schema, true); + helpers.setValue(formattedJson); + analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.OVERWRITE_SCHEMA, { + actionDescription: "Declared schema overwritten by detected schema", + stream_name: streams[testStreamIndex]?.name, + }); }} > diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/UiYamlToggleButton.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/UiYamlToggleButton.tsx index e92f1cb251261..1ca0692d9dd56 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/UiYamlToggleButton.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/UiYamlToggleButton.tsx @@ -3,6 +3,9 @@ import { FormattedMessage } from "react-intl"; import { Text } from "components/ui/Text"; +import { Action, Namespace } from "core/analytics"; +import { useAnalyticsService } from "hooks/services/Analytics"; + import styles from "./UiYamlToggleButton.module.scss"; interface UiYamlToggleButtonProps { @@ -12,8 +15,20 @@ interface UiYamlToggleButtonProps { } export const UiYamlToggleButton: React.FC = ({ className, yamlSelected, onClick }) => { + const analyticsService = useAnalyticsService(); + return ( -