Skip to content

Commit

Permalink
[Connector Builder] Add Segment event tracking (#21686)
Browse files Browse the repository at this point in the history
* direct connector builder to first workspace and fix routing

* add connector builder namespace and actions

* track connector builder start event

* track api url created event

* track auth method select

* fix action description

* track user input events

* track sidebar events

* track stream creation

* track copying stream

* track stream delete

* track stream test success/failure

* track download yaml and fix yaml file name

* track events for switching between ui/yaml

* track events for overwriting and merging schemas

* fix stream test failure/success events and add tracking of test initiation

* do not send schema contents in events

* track when stream is selected from testing panel

* fix typo

* handle initial setup completed in e2e tests

* add warning about not putting sensitive info into URL fields

* fix before in e2e test
  • Loading branch information
lmossman authored Jan 25, 2023
1 parent 4e99b5c commit 60da9e4
Show file tree
Hide file tree
Showing 18 changed files with 295 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -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();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -34,6 +36,7 @@ export const AddStreamButton: React.FC<AddStreamButtonProps> = ({
initialValues,
"data-testid": testId,
}) => {
const analyticsService = useAnalyticsService();
const { formatMessage } = useIntl();
const [isOpen, setIsOpen] = useState(false);
const [streamsField, , helpers] = useField<BuilderStream[]>("streams");
Expand All @@ -57,17 +60,35 @@ export const AddStreamButton: React.FC<AddStreamButtonProps> = ({
<Formik
initialValues={{ streamName: "", urlPath: "" }}
onSubmit={(values: AddStreamValues) => {
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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -7,12 +10,20 @@ import { BuilderOptional } from "./BuilderOptional";
import { KeyValueListField } from "./KeyValueListField";

export const AuthenticationSection: React.FC = () => {
const analyticsService = useAnalyticsService();

return (
<BuilderCard>
<BuilderOneOf
path="global.authenticator"
label="Authentication"
tooltip="Authentication method to use for requests sent to the API"
onSelect={(type) =>
analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.AUTHENTICATION_METHOD_SELECT, {
actionDescription: "Authentication method selected",
auth_type: type,
})
}
options={[
{ label: "No Auth", typeValue: "NoAuth" },
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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[] }
Expand Down Expand Up @@ -116,6 +116,9 @@ const InnerBuilderField: React.FC<BuilderFieldProps & FastFieldProps<unknown>> =
error={hasError}
readOnly={readOnly}
adornment={adornment}
onBlur={(e) => {
props.onBlur?.(e.target.value);
}}
/>
)}
{props.type === "array" && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<BuilderOneOfProps & FastFieldProps<string>> = ({
Expand All @@ -32,6 +33,7 @@ const InnerBuilderOneOf: React.FC<BuilderOneOfProps & FastFieldProps<string>> =
field: typePathField,
path,
form,
onSelect,
}) => {
const value = typePathField.value;

Expand All @@ -56,6 +58,8 @@ const InnerBuilderOneOf: React.FC<BuilderOneOfProps & FastFieldProps<string>> =
type: selectedOption.value,
...selectedOption.default,
});

onSelect?.(selectedOption.value);
}}
/>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -57,6 +59,7 @@ interface BuilderSidebarProps {
}

export const BuilderSidebar: React.FC<BuilderSidebarProps> = React.memo(({ className, toggleYamlEditor }) => {
const analyticsService = useAnalyticsService();
const { formatMessage } = useIntl();
const { hasErrors } = useBuilderErrors();
const { openConfirmationModal, closeConfirmationModal } = useConfirmationModalService();
Expand All @@ -71,6 +74,9 @@ export const BuilderSidebar: React.FC<BuilderSidebarProps> = 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",
});
},
});
};
Expand Down Expand Up @@ -100,7 +106,12 @@ export const BuilderSidebar: React.FC<BuilderSidebarProps> = 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",
});
}}
>
<FontAwesomeIcon icon={faSliders} />
<FormattedMessage id="connectorBuilder.globalConfiguration" />
Expand All @@ -111,7 +122,12 @@ export const BuilderSidebar: React.FC<BuilderSidebarProps> = 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",
});
}}
>
<FontAwesomeIcon icon={faUser} />
<FormattedMessage
Expand All @@ -131,13 +147,20 @@ export const BuilderSidebar: React.FC<BuilderSidebarProps> = React.memo(({ class
</div>

<div className={styles.streamList}>
{values.streams.map(({ name }, num) => (
{values.streams.map(({ name, id }, num) => (
<ViewSelectButton
key={num}
data-testid={`navbutton-${String(num)}`}
selected={selectedView === num}
showErrorIndicator={hasErrors(true, [num])}
onClick={() => 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() ? (
<Text className={styles.streamViewText}>{name}</Text>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -9,13 +14,29 @@ import styles from "./GlobalConfigView.module.scss";

export const GlobalConfigView: React.FC = () => {
const { formatMessage } = useIntl();
const analyticsService = useAnalyticsService();

return (
<BuilderConfigView heading={formatMessage({ id: "connectorBuilder.globalConfiguration" })}>
{/* Not using intl for the labels and tooltips in this component in order to keep maintainence simple */}
<BuilderTitle path="global.connectorName" label="Connector Name" size="lg" />
<BuilderCard className={styles.content}>
<BuilderField type="string" path="global.urlBase" label="API URL" tooltip="Base URL of the source API" />
<BuilderField
type="string"
path="global.urlBase"
label="API URL"
tooltip={
<TextWithHTML text="Base URL of the source API.<br><br>Do not put sensitive information (e.g. API tokens) into this field - use the Authentication component for this." />
}
onBlur={(value: string) => {
if (value) {
analyticsService.track(Namespace.CONNECTOR_BUILDER, Action.API_URL_CREATE, {
actionDescription: "Base API URL filled in",
api_url: value,
});
}
}}
/>
</BuilderCard>
<AuthenticationSection />
</BuilderConfigView>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -68,6 +70,7 @@ export const InputForm = ({
inputInEditing: InputInEditing;
onClose: (newInput?: BuilderFormInput) => void;
}) => {
const analyticsService = useAnalyticsService();
const { values, setFieldValue } = useFormikContext<BuilderFormValues>();
const [inputs, , helpers] = useField<BuilderFormInput[]>("inputs");
const inferredInputs = useMemo(
Expand Down Expand Up @@ -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,
}
);
}}
>
<>
Expand All @@ -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();
Expand Down
Loading

0 comments on commit 60da9e4

Please sign in to comment.