);
})}
From 6d4be65205d11aae01cb0bf0d6fdbf0fe7739d9d Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Wed, 17 May 2023 14:55:54 -0400
Subject: [PATCH 05/39] Adds some flexibility to the SQl queries to handle
different table names for the tracked events, along with different event
column names based on the schemaFormat.
---
packages/back-end/src/controllers/metrics.ts | 10 ++++-
.../back-end/src/integrations/BigQuery.ts | 4 +-
.../src/integrations/SqlIntegration.ts | 44 ++++++++++++-------
packages/back-end/src/types/Integration.ts | 6 +--
4 files changed, 40 insertions(+), 24 deletions(-)
diff --git a/packages/back-end/src/controllers/metrics.ts b/packages/back-end/src/controllers/metrics.ts
index d2340709b86..bfdc5a4a769 100644
--- a/packages/back-end/src/controllers/metrics.ts
+++ b/packages/back-end/src/controllers/metrics.ts
@@ -483,7 +483,11 @@ export async function getAutoMetrics(
const integration = getSourceIntegrationObject(dataSourceObj);
- if (!integration.getTrackedEvents) {
+ if (
+ !integration.getTrackedEvents ||
+ !integration.settings.schemaFormat ||
+ integration.settings.schemaFormat === "custom" //MKTODO: Is this logic correct?
+ ) {
//TODO: Is this the correct error code?
res.status(403).json({
status: 403,
@@ -493,7 +497,9 @@ export async function getAutoMetrics(
}
try {
- const results = await integration.getTrackedEvents();
+ const results = await integration.getTrackedEvents(
+ integration.settings.schemaFormat
+ );
if (results.length) {
return res.status(200).json({
diff --git a/packages/back-end/src/integrations/BigQuery.ts b/packages/back-end/src/integrations/BigQuery.ts
index 3d3cdb8d183..496a0f4dae7 100644
--- a/packages/back-end/src/integrations/BigQuery.ts
+++ b/packages/back-end/src/integrations/BigQuery.ts
@@ -99,7 +99,7 @@ export default class BigQuery extends SqlIntegration {
): string {
return `\`${databaseName}.${tableSchema}.INFORMATION_SCHEMA.COLUMNS\``;
}
- getTrackedEventsFromClause(): string {
+ getTrackedEventsFromClause(trackedEventTableName: string): string {
if (!this.params.projectId)
throw new Error(
"No projectId provided. In order to get the information schema, you must provide a projectId."
@@ -108,7 +108,7 @@ export default class BigQuery extends SqlIntegration {
throw new MissingDatasourceParamsError(
"To view the information schema for a BigQuery dataset, you must define a default dataset. Please add a default dataset by editing the datasource's connection settings."
);
- return `\`${this.params.projectId}.${this.params.defaultDataset}.tracks\``;
+ return `\`${this.params.projectId}.${this.params.defaultDataset}.${trackedEventTableName}\``;
}
getAutomaticMetricFromClause(event: string): string {
diff --git a/packages/back-end/src/integrations/SqlIntegration.ts b/packages/back-end/src/integrations/SqlIntegration.ts
index a6a43b54b27..c8ec9d37785 100644
--- a/packages/back-end/src/integrations/SqlIntegration.ts
+++ b/packages/back-end/src/integrations/SqlIntegration.ts
@@ -1290,25 +1290,34 @@ export default abstract class SqlIntegration
return { tableData };
}
- getTrackedEventsFromClause(): string {
- return "TODO";
+ getTrackedEventsFromClause(trackedEventTableName: string): string {
+ return trackedEventTableName;
}
- async getTrackedEvents(): Promise<
- { event: string; hasUserId: boolean; createForUser: boolean }[]
- > {
+ async getTrackedEvents(
+ schemaFormat: SchemaFormat
+ ): Promise<{ event: string; hasUserId: boolean; createForUser: boolean }[]> {
+ let eventColumn;
+ let trackedEventTableName;
+
+ switch (schemaFormat) {
+ //TODO: Add cases for schemaFormats where the tracked events aren't stored in a column called "events"
+ default:
+ eventColumn = "event";
+ trackedEventTableName = "tracks";
+ }
+
const sql = `
SELECT
- event,
- (CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId
+ ${eventColumn} as event,
+ (CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId,
+ (CASE WHEN (1 > 0) THEN 1 ELSE 0 END) as createForUser
FROM
- ${this.getTrackedEventsFromClause()}
+ ${this.getTrackedEventsFromClause(trackedEventTableName)}
GROUP BY event`;
const results = await this.runQuery(format(sql, this.getFormatDialect()));
- results.forEach((result) => (result.createForUser = true));
-
if (!results) {
throw new Error(`No events found.`);
}
@@ -1316,15 +1325,16 @@ export default abstract class SqlIntegration
return results;
}
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
getAutomaticMetricFromClause(event: string): string {
- return "TODO";
+ return event;
}
getAutomaticMetricTimestampColumn(schemaFormat: SchemaFormat): string {
switch (schemaFormat) {
- case "segment":
- return "loaded_at";
+ case "segment" || "rudderstack":
+ return "received_at";
+ case "amplitude":
+ return "server_upload_time";
default:
return "timestamp";
}
@@ -1352,9 +1362,9 @@ export default abstract class SqlIntegration
): string {
const sqlQuery = `
SELECT
- ${
- metric.hasUserId ? "user_id, " : ""
- }anonymous_id, ${this.getAutomaticMetricTimestampColumn(
+ ${metric.hasUserId ? "user_id, " : ""}${
+ schemaFormat === "amplitude" ? "amplitude_id" : "anonymous_id"
+ } as anonymous_id, ${this.getAutomaticMetricTimestampColumn(
schemaFormat
)} as timestamp
${this.getAutomaticMetricAggregatorColumn(
diff --git a/packages/back-end/src/types/Integration.ts b/packages/back-end/src/types/Integration.ts
index 5a3e9216154..18c5fd828c0 100644
--- a/packages/back-end/src/types/Integration.ts
+++ b/packages/back-end/src/types/Integration.ts
@@ -293,9 +293,9 @@ export interface SourceIntegrationInterface {
query: string
): Promise;
runPastExperimentQuery(query: string): Promise;
- getTrackedEvents?: () => Promise<
- { event: string; hasUserId: boolean; createForUser: boolean }[]
- >;
+ getTrackedEvents?: (
+ schemaFormat: SchemaFormat
+ ) => Promise<{ event: string; hasUserId: boolean; createForUser: boolean }[]>;
getAutomaticMetricSqlQuery?(
metric: {
event: string;
From 75aba06b4e25ca857d7d9ad79c38c3140d90e8ce Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Wed, 17 May 2023 16:34:37 -0400
Subject: [PATCH 06/39] Overall cleanup and renaming of methods.
---
packages/back-end/src/app.ts | 5 +-
.../back-end/src/controllers/datasources.ts | 68 +++++++++-
packages/back-end/src/controllers/metrics.ts | 63 ---------
.../back-end/src/integrations/BigQuery.ts | 4 +-
.../src/integrations/SqlIntegration.ts | 35 +++--
.../src/jobs/createAutomaticMetrics.ts | 128 +++++++-----------
packages/back-end/src/types/Integration.ts | 4 +-
packages/back-end/types/datasource.d.ts | 1 +
.../components/Settings/NewDataSourceForm.tsx | 2 +-
9 files changed, 148 insertions(+), 162 deletions(-)
diff --git a/packages/back-end/src/app.ts b/packages/back-end/src/app.ts
index 41227826841..962efbf103c 100644
--- a/packages/back-end/src/app.ts
+++ b/packages/back-end/src/app.ts
@@ -346,7 +346,6 @@ app.get(
metricsController.getMetricAnalysisStatus
);
app.post("/metric/:id/analysis/cancel", metricsController.cancelMetricAnalysis);
-app.get("/metrics/generate/:dataSourceId", metricsController.getAutoMetrics);
// Experiments
app.get("/experiments", experimentsController.getExperiments);
@@ -483,6 +482,10 @@ app.get("/datasource/:id", datasourcesController.getDataSource);
app.post("/datasources", datasourcesController.postDataSources);
app.put("/datasource/:id", datasourcesController.putDataSource);
app.delete("/datasource/:id", datasourcesController.deleteDataSource);
+app.get(
+ "/datasource/:id/auto-metrics",
+ datasourcesController.getAutoGeneratedMetricsToCreate
+);
// Information Schemas
app.get(
diff --git a/packages/back-end/src/controllers/datasources.ts b/packages/back-end/src/controllers/datasources.ts
index 3c45d9862de..f2338a8acdd 100644
--- a/packages/back-end/src/controllers/datasources.ts
+++ b/packages/back-end/src/controllers/datasources.ts
@@ -442,9 +442,6 @@ export async function putDataSource(
metricsToCreate,
} = req.body;
- console.log("made it to the putDataSource method");
- console.log("metricsToCreate", metricsToCreate);
-
const datasource = await getDataSourceById(id, org.id);
if (!datasource) {
res.status(404).json({
@@ -622,3 +619,68 @@ export async function testLimitedQuery(
error,
});
}
+
+export async function getAutoGeneratedMetricsToCreate(
+ req: AuthRequest,
+ res: Response
+) {
+ const { org } = getOrgFromReq(req);
+ const { id } = req.params;
+
+ const dataSourceObj = await getDataSourceById(id, org.id);
+ if (!dataSourceObj) {
+ res.status(403).json({
+ status: 403,
+ message: "Invalid data source: " + id,
+ });
+ return;
+ }
+
+ req.checkPermissions(
+ "createMetrics",
+ dataSourceObj.projects?.length ? dataSourceObj.projects : ""
+ );
+
+ const integration = getSourceIntegrationObject(dataSourceObj);
+
+ if (
+ !integration.getEventsTrackedByDatasource ||
+ !integration.settings.schemaFormat ||
+ !integration.getSourceProperties().supportsAutoGeneratedMetrics ||
+ integration.settings.schemaFormat === "custom" //MKTODO: Is this logic correct?
+ ) {
+ //TODO: Is this the correct error code?
+ res.status(403).json({
+ status: 403,
+ message: "This datasource does not support automatic metric generation.",
+ });
+ return;
+ }
+
+ try {
+ // Get the list of events tracked by this datasource - we can create metrics from these events.
+ const results = await integration.getEventsTrackedByDatasource(
+ integration.settings.schemaFormat
+ );
+
+ if (results.length) {
+ return res.status(200).json({
+ status: 200,
+ results,
+ });
+ } else {
+ return res.status(200).json({
+ status: 200,
+ results,
+ message: "No events found.",
+ });
+ }
+ } catch (e) {
+ res.status(400).json({
+ status: 400,
+ results: [],
+ message: e.message,
+ });
+ return;
+ }
+}
diff --git a/packages/back-end/src/controllers/metrics.ts b/packages/back-end/src/controllers/metrics.ts
index bfdc5a4a769..7d5aa2aa9ce 100644
--- a/packages/back-end/src/controllers/metrics.ts
+++ b/packages/back-end/src/controllers/metrics.ts
@@ -459,66 +459,3 @@ export async function putMetric(
}),
});
}
-
-export async function getAutoMetrics(
- req: AuthRequest,
- res: Response
-) {
- const { org } = getOrgFromReq(req);
- const { dataSourceId } = req.params;
-
- const dataSourceObj = await getDataSourceById(dataSourceId, org.id);
- if (!dataSourceObj) {
- res.status(403).json({
- status: 403,
- message: "Invalid data source: " + dataSourceId,
- });
- return;
- }
-
- req.checkPermissions(
- "createMetrics",
- dataSourceObj.projects?.length ? dataSourceObj.projects : ""
- );
-
- const integration = getSourceIntegrationObject(dataSourceObj);
-
- if (
- !integration.getTrackedEvents ||
- !integration.settings.schemaFormat ||
- integration.settings.schemaFormat === "custom" //MKTODO: Is this logic correct?
- ) {
- //TODO: Is this the correct error code?
- res.status(403).json({
- status: 403,
- message: "This datasource does not support automatic metric generation.",
- });
- return;
- }
-
- try {
- const results = await integration.getTrackedEvents(
- integration.settings.schemaFormat
- );
-
- if (results.length) {
- return res.status(200).json({
- status: 200,
- results,
- });
- } else {
- return res.status(200).json({
- status: 200,
- results,
- message: "No events found.",
- });
- }
- } catch (e) {
- res.status(400).json({
- status: 400,
- results: [],
- message: e.message,
- });
- return;
- }
-}
diff --git a/packages/back-end/src/integrations/BigQuery.ts b/packages/back-end/src/integrations/BigQuery.ts
index 496a0f4dae7..4d5046106e1 100644
--- a/packages/back-end/src/integrations/BigQuery.ts
+++ b/packages/back-end/src/integrations/BigQuery.ts
@@ -99,7 +99,9 @@ export default class BigQuery extends SqlIntegration {
): string {
return `\`${databaseName}.${tableSchema}.INFORMATION_SCHEMA.COLUMNS\``;
}
- getTrackedEventsFromClause(trackedEventTableName: string): string {
+ getEventsTrackedByDatasourceFromClause(
+ trackedEventTableName: string
+ ): string {
if (!this.params.projectId)
throw new Error(
"No projectId provided. In order to get the information schema, you must provide a projectId."
diff --git a/packages/back-end/src/integrations/SqlIntegration.ts b/packages/back-end/src/integrations/SqlIntegration.ts
index c8ec9d37785..5972bddef76 100644
--- a/packages/back-end/src/integrations/SqlIntegration.ts
+++ b/packages/back-end/src/integrations/SqlIntegration.ts
@@ -80,6 +80,7 @@ export default abstract class SqlIntegration
activationDimension: true,
pastExperiments: true,
supportsInformationSchema: true,
+ supportsAutoGeneratedMetrics: this.isAutoGeneratedMetricSupported(),
};
}
@@ -88,6 +89,18 @@ export default abstract class SqlIntegration
return true;
}
+ isAutoGeneratedMetricSupported(): boolean {
+ const supportedEventTrackers = ["segment"];
+
+ if (
+ this.settings.schemaFormat &&
+ supportedEventTrackers.includes(this.settings.schemaFormat)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
getSchema(): string {
return "";
}
@@ -1290,11 +1303,13 @@ export default abstract class SqlIntegration
return { tableData };
}
- getTrackedEventsFromClause(trackedEventTableName: string): string {
+ getEventsTrackedByDatasourceFromClause(
+ trackedEventTableName: string
+ ): string {
return trackedEventTableName;
}
- async getTrackedEvents(
+ async getEventsTrackedByDatasource(
schemaFormat: SchemaFormat
): Promise<{ event: string; hasUserId: boolean; createForUser: boolean }[]> {
let eventColumn;
@@ -1313,7 +1328,7 @@ export default abstract class SqlIntegration
(CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId,
(CASE WHEN (1 > 0) THEN 1 ELSE 0 END) as createForUser
FROM
- ${this.getTrackedEventsFromClause(trackedEventTableName)}
+ ${this.getEventsTrackedByDatasourceFromClause(trackedEventTableName)}
GROUP BY event`;
const results = await this.runQuery(format(sql, this.getFormatDialect()));
@@ -1325,11 +1340,11 @@ export default abstract class SqlIntegration
return results;
}
- getAutomaticMetricFromClause(event: string): string {
+ getAutoGeneratedMetricFromClause(event: string): string {
return event;
}
- getAutomaticMetricTimestampColumn(schemaFormat: SchemaFormat): string {
+ getAutoGeneratedMetricTimestampColumn(schemaFormat: SchemaFormat): string {
switch (schemaFormat) {
case "segment" || "rudderstack":
return "received_at";
@@ -1340,7 +1355,7 @@ export default abstract class SqlIntegration
}
}
- getAutomaticMetricAggregatorColumn(metricType: MetricType): string {
+ getAutoGeneratedMetricValueColumn(metricType: MetricType): string {
switch (metricType) {
case "count":
return ", count as value";
@@ -1351,7 +1366,7 @@ export default abstract class SqlIntegration
}
}
- getAutomaticMetricSqlQuery(
+ getAutoGeneratedMetricSqlQuery(
metric: {
event: string;
hasUserId: boolean;
@@ -1364,12 +1379,12 @@ export default abstract class SqlIntegration
SELECT
${metric.hasUserId ? "user_id, " : ""}${
schemaFormat === "amplitude" ? "amplitude_id" : "anonymous_id"
- } as anonymous_id, ${this.getAutomaticMetricTimestampColumn(
+ } as anonymous_id, ${this.getAutoGeneratedMetricTimestampColumn(
schemaFormat
)} as timestamp
- ${this.getAutomaticMetricAggregatorColumn(
+ ${this.getAutoGeneratedMetricValueColumn(
metricType
- )} FROM ${this.getAutomaticMetricFromClause(metric.event)}
+ )} FROM ${this.getAutoGeneratedMetricFromClause(metric.event)}
`;
return format(sqlQuery, this.getFormatDialect());
diff --git a/packages/back-end/src/jobs/createAutomaticMetrics.ts b/packages/back-end/src/jobs/createAutomaticMetrics.ts
index acfb0edb569..1c1a3752d2c 100644
--- a/packages/back-end/src/jobs/createAutomaticMetrics.ts
+++ b/packages/back-end/src/jobs/createAutomaticMetrics.ts
@@ -1,3 +1,4 @@
+/* eslint-disable no-console */
import Agenda, { Job } from "agenda";
import uniqid from "uniqid";
import { getDataSourceById } from "../models/DataSourceModel";
@@ -5,7 +6,6 @@ import { insertMetrics } from "../models/MetricModel";
import { MetricInterface, MetricType } from "../../types/metric";
import { getSourceIntegrationObject } from "../services/datasource";
import { getInformationSchemaById } from "../models/InformationSchemaModel";
-import { getInformationSchemaTableById } from "../models/InformationSchemaTablesModel";
import { fetchTableData } from "../services/informationSchema";
import { getPath } from "../util/informationSchemas";
import { Column } from "../types/Integration";
@@ -29,6 +29,7 @@ export default function (ag: Agenda) {
agenda.define(
CREATE_AUTOMATIC_METRICS_JOB_NAME,
async (job: CreateAutomaticMetricsJob) => {
+ console.log("made it to the job");
const { datasourceId, organization, metricsToCreate } = job.attrs.data;
if (!datasourceId || !organization || !metricsToCreate) return;
@@ -39,35 +40,29 @@ export default function (ag: Agenda) {
const integration = getSourceIntegrationObject(datasource);
- try {
- console.log("made it to the try block");
- const metrics: Partial[] = [];
- for (const metric of metricsToCreate) {
- console.log("metric", metric);
- // sampleMetric = {event: "signed_up", hasUserId: true, createForUser: true};
- if (metric.createForUser) {
- // We need to build the SQL query for the metric
- if (!integration.getAutomaticMetricSqlQuery) return; //TODO: Throw an error?
- // But before we can do that, we need to know if the event (aka, the table_name) has a revenue or count column
- // If so, that changes the type, which also affects the query
+ if (
+ !integration.getAutoGeneratedMetricSqlQuery ||
+ !integration.getSourceProperties().supportsAutoGeneratedMetrics
+ )
+ return;
- // To do that, I need to get the `information_schema_table`'s id, and then get all of the columns for that table.
- const informationSchemaId = datasource.settings.informationSchemaId;
- console.log("informationSchemaId", informationSchemaId);
+ const informationSchemaId = datasource.settings.informationSchemaId;
- if (!informationSchemaId) return; //TODO: Throw an error?
+ if (!informationSchemaId) return; //TODO: Throw an error?
- const informationSchema = await getInformationSchemaById(
- organization,
- informationSchemaId
- );
+ const informationSchema = await getInformationSchemaById(
+ organization,
+ informationSchemaId
+ );
- console.log("informationSchema", informationSchema);
+ if (!informationSchema) return; //TODO: Throw an error?
- if (!informationSchema) return; //TODO: Throw an error?
-
- let informationSchemaTableId = "";
+ let informationSchemaTableId = "";
+ try {
+ const metrics: Partial[] = [];
+ for (const metric of metricsToCreate) {
+ if (metric.createForUser) {
informationSchema.databases.forEach((database) => {
database.schemas.forEach((schema) => {
schema.tables.forEach((table) => {
@@ -78,75 +73,46 @@ export default function (ag: Agenda) {
});
});
- console.log("informationSchemaTableId", informationSchemaTableId);
-
- const informationSchemaTable = await getInformationSchemaTableById(
- organization,
+ const {
+ tableData,
+ databaseName,
+ tableSchema,
+ tableName,
+ } = await fetchTableData(
+ datasource,
+ informationSchema,
informationSchemaTableId
);
- console.log("informationSchemaTable", informationSchemaTable);
-
- let metricType: MetricType = "binomial";
-
- if (!informationSchemaTable) {
- // I need to fetch it and set it within Mongo,
- const {
- tableData,
- databaseName,
- tableSchema,
- tableName,
- } = await fetchTableData(
- datasource,
- informationSchema,
- informationSchemaTableId
- );
-
- if (!tableData) return; //TODO: Throw an error?
-
- const columns: Column[] = tableData?.map(
- (row: { column_name: string; data_type: string }) => {
- return {
+ if (!tableData) return; //TODO: Throw an error?
+
+ const columns: Column[] = tableData?.map(
+ (row: { column_name: string; data_type: string }) => {
+ return {
+ columnName: row.column_name,
+ dataType: row.data_type,
+ path: getPath(datasource.type, {
+ tableCatalog: databaseName,
+ tableSchema: tableSchema,
+ tableName: tableName,
columnName: row.column_name,
- dataType: row.data_type,
- path: getPath(datasource.type, {
- tableCatalog: databaseName,
- tableSchema: tableSchema,
- tableName: tableName,
- columnName: row.column_name,
- }),
- };
- }
- );
-
- if (columns.length) {
- if (columns.some((column) => column.columnName === "revenue")) {
- metricType = "revenue";
- } else if (
- columns.some((column) => column.columnName === "count")
- ) {
- metricType = "count";
- }
+ }),
+ };
}
- }
+ );
- if (informationSchemaTable?.columns.length) {
- if (
- informationSchemaTable.columns.some(
- (column) => column.columnName === "revenue"
- )
- ) {
+ let metricType: MetricType = "binomial";
+
+ if (columns.length) {
+ if (columns.some((column) => column.columnName === "revenue")) {
metricType = "revenue";
} else if (
- informationSchemaTable.columns.some(
- (column) => column.columnName === "count"
- )
+ columns.some((column) => column.columnName === "count")
) {
metricType = "count";
}
}
-
- const sqlQuery = integration.getAutomaticMetricSqlQuery(
+ const sqlQuery = integration.getAutoGeneratedMetricSqlQuery(
metric,
integration.settings.schemaFormat || "custom",
metricType
diff --git a/packages/back-end/src/types/Integration.ts b/packages/back-end/src/types/Integration.ts
index 18c5fd828c0..48e8ead6bc1 100644
--- a/packages/back-end/src/types/Integration.ts
+++ b/packages/back-end/src/types/Integration.ts
@@ -293,10 +293,10 @@ export interface SourceIntegrationInterface {
query: string
): Promise;
runPastExperimentQuery(query: string): Promise;
- getTrackedEvents?: (
+ getEventsTrackedByDatasource?: (
schemaFormat: SchemaFormat
) => Promise<{ event: string; hasUserId: boolean; createForUser: boolean }[]>;
- getAutomaticMetricSqlQuery?(
+ getAutoGeneratedMetricSqlQuery?(
metric: {
event: string;
hasUserId: boolean;
diff --git a/packages/back-end/types/datasource.d.ts b/packages/back-end/types/datasource.d.ts
index ae8d436d505..64fffc0ebef 100644
--- a/packages/back-end/types/datasource.d.ts
+++ b/packages/back-end/types/datasource.d.ts
@@ -95,6 +95,7 @@ export interface DataSourceProperties {
pastExperiments?: boolean;
separateExperimentResultQueries?: boolean;
supportsInformationSchema?: boolean;
+ supportsAutoGeneratedMetrics?: boolean;
}
type WithParams = Omit & {
diff --git a/packages/front-end/components/Settings/NewDataSourceForm.tsx b/packages/front-end/components/Settings/NewDataSourceForm.tsx
index 774fdb53b93..6e2dfe263a3 100644
--- a/packages/front-end/components/Settings/NewDataSourceForm.tsx
+++ b/packages/front-end/components/Settings/NewDataSourceForm.tsx
@@ -558,7 +558,7 @@ const NewDataSourceForm: FC<{
createForUser: boolean;
}[];
message?: string;
- }>(`/metrics/generate/${dataSourceId}`, {
+ }>(`/datasource/${dataSourceId}/auto-metrics`, {
method: "GET",
});
if (res.message) {
From 3503ef47e9a947b07ae3c5cb0314397aeef79a28 Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Thu, 18 May 2023 06:50:02 -0400
Subject: [PATCH 07/39] Removing unused import.
---
packages/back-end/src/controllers/metrics.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/back-end/src/controllers/metrics.ts b/packages/back-end/src/controllers/metrics.ts
index 7d5aa2aa9ce..272126412a9 100644
--- a/packages/back-end/src/controllers/metrics.ts
+++ b/packages/back-end/src/controllers/metrics.ts
@@ -31,7 +31,6 @@ import {
auditDetailsDelete,
} from "../services/audit";
import { EventAuditUserForResponseLocals } from "../events/event-types";
-import { getSourceIntegrationObject } from "../services/datasource";
export async function deleteMetric(
req: AuthRequest,
From 4935fa38127ebb71b1bff00f5cb49afc2756e9a9 Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Thu, 18 May 2023 09:52:07 -0400
Subject: [PATCH 08/39] Updates the front end to consume the new
supportsAutoGeneratedMetrics integration property.
---
.../back-end/src/controllers/datasources.ts | 15 ++++---
.../back-end/src/integrations/BigQuery.ts | 2 +-
.../components/Settings/NewDataSourceForm.tsx | 41 +++++++++++++++----
3 files changed, 40 insertions(+), 18 deletions(-)
diff --git a/packages/back-end/src/controllers/datasources.ts b/packages/back-end/src/controllers/datasources.ts
index f2338a8acdd..4ccc7dfb826 100644
--- a/packages/back-end/src/controllers/datasources.ts
+++ b/packages/back-end/src/controllers/datasources.ts
@@ -646,8 +646,7 @@ export async function getAutoGeneratedMetricsToCreate(
if (
!integration.getEventsTrackedByDatasource ||
!integration.settings.schemaFormat ||
- !integration.getSourceProperties().supportsAutoGeneratedMetrics ||
- integration.settings.schemaFormat === "custom" //MKTODO: Is this logic correct?
+ !integration.getSourceProperties().supportsAutoGeneratedMetrics
) {
//TODO: Is this the correct error code?
res.status(403).json({
@@ -663,18 +662,18 @@ export async function getAutoGeneratedMetricsToCreate(
integration.settings.schemaFormat
);
- if (results.length) {
- return res.status(200).json({
- status: 200,
- results,
- });
- } else {
+ if (!results.length) {
return res.status(200).json({
status: 200,
results,
message: "No events found.",
});
}
+
+ return res.status(200).json({
+ status: 200,
+ results,
+ });
} catch (e) {
res.status(400).json({
status: 400,
diff --git a/packages/back-end/src/integrations/BigQuery.ts b/packages/back-end/src/integrations/BigQuery.ts
index 4d5046106e1..2a356443817 100644
--- a/packages/back-end/src/integrations/BigQuery.ts
+++ b/packages/back-end/src/integrations/BigQuery.ts
@@ -113,7 +113,7 @@ export default class BigQuery extends SqlIntegration {
return `\`${this.params.projectId}.${this.params.defaultDataset}.${trackedEventTableName}\``;
}
- getAutomaticMetricFromClause(event: string): string {
+ getAutoGeneratedMetricFromClause(event: string): string {
if (!this.params.projectId)
throw new Error(
"No projectId provided. In order to get the information schema, you must provide a projectId."
diff --git a/packages/front-end/components/Settings/NewDataSourceForm.tsx b/packages/front-end/components/Settings/NewDataSourceForm.tsx
index 6e2dfe263a3..c34f4316f98 100644
--- a/packages/front-end/components/Settings/NewDataSourceForm.tsx
+++ b/packages/front-end/components/Settings/NewDataSourceForm.tsx
@@ -50,7 +50,12 @@ const NewDataSourceForm: FC<{
inline,
secondaryCTA,
}) => {
- const { projects, project } = useDefinitions();
+ const {
+ projects,
+ project,
+ getDatasourceById,
+ mutateDefinitions,
+ } = useDefinitions();
const [step, setStep] = useState(0);
const [schema, setSchema] = useState("");
const [dataSourceId, setDataSourceId] = useState(
@@ -79,6 +84,27 @@ const NewDataSourceForm: FC<{
settings: {},
};
+ const [
+ datasourceSupportsAutoGeneratedMetrics,
+ setDatasourceSupportsAutoGeneratedMetrics,
+ ] = useState(false);
+
+ useEffect(() => {
+ if (dataSourceId) {
+ mutateDefinitions();
+ const datasourceObj = getDatasourceById(dataSourceId);
+ const supportsAutoGeneratedMetrics =
+ datasourceObj?.properties?.supportsAutoGeneratedMetrics || false;
+ setDatasourceSupportsAutoGeneratedMetrics(supportsAutoGeneratedMetrics);
+ }
+ }, [
+ dataSourceId,
+ datasource.properties?.supportsAutoGeneratedMetrics,
+ datasourceSupportsAutoGeneratedMetrics,
+ getDatasourceById,
+ mutateDefinitions,
+ ]);
+
const form = useForm<{
settings: DataSourceSettings | undefined;
metricsToCreate: {
@@ -242,11 +268,6 @@ const NewDataSourceForm: FC<{
});
};
- const metricsToCreateForUser = form.watch("metricsToCreate");
-
- console.log("form.watch('metricsToCreate')", form.watch("metricsToCreate"));
- console.log("metricsToCreateForUser", metricsToCreateForUser);
-
const setSchemaSettings = (s: eventSchema) => {
setSchema(s.value);
form.setValue("settings.schemaFormat", s.value);
@@ -534,14 +555,16 @@ const NewDataSourceForm: FC<{
/>
))}
- {schemasMap.get(schema)?.label === "Segment" && (
+ {datasourceSupportsAutoGeneratedMetrics && (
Metric Options
{metricsToCreate.length === 0 ? (
- With Segment, we may be able to generate metrics for you
- automatically,{" "}
+ {`With ${
+ schemasMap.get(schema).label
+ }, we may be able to generate metrics for you
+ automatically,`}{" "}
saving you and your team valuable time. (It's Free)
From eff2b945a717f9bde2cc5dbb9e559629eb0f5d86 Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Thu, 18 May 2023 11:01:41 -0400
Subject: [PATCH 09/39] Expanding support for all data warehouses for Segment &
Rudderstack - not fully tested yet.
---
packages/back-end/src/integrations/Athena.ts | 9 +++++++++
packages/back-end/src/integrations/BigQuery.ts | 8 ++++----
packages/back-end/src/integrations/Mssql.ts | 9 +++++++++
packages/back-end/src/integrations/Snowflake.ts | 9 +++++++++
.../back-end/src/integrations/SqlIntegration.ts | 14 ++++++++++----
5 files changed, 41 insertions(+), 8 deletions(-)
diff --git a/packages/back-end/src/integrations/Athena.ts b/packages/back-end/src/integrations/Athena.ts
index c84f583e9fb..fb491278c2a 100644
--- a/packages/back-end/src/integrations/Athena.ts
+++ b/packages/back-end/src/integrations/Athena.ts
@@ -59,4 +59,13 @@ export default class Athena extends SqlIntegration {
getInformationSchemaTableFromClause(databaseName: string): string {
return `${databaseName}.information_schema.columns`;
}
+ getEventsTrackedByDatasourceFromClause(
+ trackedEventTableName: string
+ ): string {
+ if (!this.params.catalog)
+ throw new MissingDatasourceParamsError(
+ "To automatically generate metrics for an Athena data source, you must define a default catalog."
+ );
+ return `${this.params.catalog}.${trackedEventTableName}`;
+ }
}
diff --git a/packages/back-end/src/integrations/BigQuery.ts b/packages/back-end/src/integrations/BigQuery.ts
index 2a356443817..ebb6881ce58 100644
--- a/packages/back-end/src/integrations/BigQuery.ts
+++ b/packages/back-end/src/integrations/BigQuery.ts
@@ -104,11 +104,11 @@ export default class BigQuery extends SqlIntegration {
): string {
if (!this.params.projectId)
throw new Error(
- "No projectId provided. In order to get the information schema, you must provide a projectId."
+ "No projectId provided. To automatically generate metrics you must provide a projectId."
);
if (!this.params.defaultDataset)
throw new MissingDatasourceParamsError(
- "To view the information schema for a BigQuery dataset, you must define a default dataset. Please add a default dataset by editing the datasource's connection settings."
+ "To automatically generate metrics for a BigQuery dataset, you must define a default dataset."
);
return `\`${this.params.projectId}.${this.params.defaultDataset}.${trackedEventTableName}\``;
}
@@ -116,11 +116,11 @@ export default class BigQuery extends SqlIntegration {
getAutoGeneratedMetricFromClause(event: string): string {
if (!this.params.projectId)
throw new Error(
- "No projectId provided. In order to get the information schema, you must provide a projectId."
+ "No projectId provided. To automatically generate metrics you must provide a projectId."
);
if (!this.params.defaultDataset)
throw new MissingDatasourceParamsError(
- "To view the information schema for a BigQuery dataset, you must define a default dataset. Please add a default dataset by editing the datasource's connection settings."
+ "To automatically generate metrics for a BigQuery dataset, you must define a default dataset."
);
return `\`${this.params.projectId}.${this.params.defaultDataset}.${event}\``;
}
diff --git a/packages/back-end/src/integrations/Mssql.ts b/packages/back-end/src/integrations/Mssql.ts
index 69200f8d907..e154e621e41 100644
--- a/packages/back-end/src/integrations/Mssql.ts
+++ b/packages/back-end/src/integrations/Mssql.ts
@@ -77,4 +77,13 @@ export default class Mssql extends SqlIntegration {
getInformationSchemaTableFromClause(databaseName: string): string {
return `${databaseName}.information_schema.columns`;
}
+ getEventsTrackedByDatasourceFromClause(
+ trackedEventTableName: string
+ ): string {
+ if (!this.params.database)
+ throw new Error(
+ "No database provided. To automatically generate metrics, you must provide a database."
+ );
+ return `${this.params.database}.${trackedEventTableName}`;
+ }
}
diff --git a/packages/back-end/src/integrations/Snowflake.ts b/packages/back-end/src/integrations/Snowflake.ts
index 31d406016e2..b9514fbf44e 100644
--- a/packages/back-end/src/integrations/Snowflake.ts
+++ b/packages/back-end/src/integrations/Snowflake.ts
@@ -47,4 +47,13 @@ export default class Snowflake extends SqlIntegration {
getInformationSchemaTableFromClause(databaseName: string): string {
return `${databaseName}.information_schema.columns`;
}
+ getEventsTrackedByDatasourceFromClause(
+ trackedEventTableName: string
+ ): string {
+ if (!this.params.database)
+ throw new Error(
+ "No database provided. To automatically generate metrics, you must provide a database."
+ );
+ return `${this.params.database}.${trackedEventTableName}`;
+ }
}
diff --git a/packages/back-end/src/integrations/SqlIntegration.ts b/packages/back-end/src/integrations/SqlIntegration.ts
index 5972bddef76..478e8612961 100644
--- a/packages/back-end/src/integrations/SqlIntegration.ts
+++ b/packages/back-end/src/integrations/SqlIntegration.ts
@@ -90,7 +90,7 @@ export default abstract class SqlIntegration
}
isAutoGeneratedMetricSupported(): boolean {
- const supportedEventTrackers = ["segment"];
+ const supportedEventTrackers: SchemaFormat[] = ["segment", "rudderstack"];
if (
this.settings.schemaFormat &&
@@ -1355,10 +1355,15 @@ export default abstract class SqlIntegration
}
}
- getAutoGeneratedMetricValueColumn(metricType: MetricType): string {
+ getAutoGeneratedMetricValueColumn(
+ metricType: MetricType,
+ schemaFormat: SchemaFormat
+ ): string {
+ const countColumn = schemaFormat === "rudderstack" ? "value" : "count";
+
switch (metricType) {
case "count":
- return ", count as value";
+ return `, ${countColumn} as value`;
case "revenue":
return ", revenue as value";
default:
@@ -1383,7 +1388,8 @@ export default abstract class SqlIntegration
schemaFormat
)} as timestamp
${this.getAutoGeneratedMetricValueColumn(
- metricType
+ metricType,
+ schemaFormat
)} FROM ${this.getAutoGeneratedMetricFromClause(metric.event)}
`;
From 775335866ba8cda5b527dca35cb6f7b22ae7a456 Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Thu, 18 May 2023 16:20:20 -0400
Subject: [PATCH 10/39] Updates the mssql integration to include an optional
default schema input as that is required for us to know which schema to query
when looking for the tracked events coming from Rudderstack. Also updating
the Snowflake from clause for the query to use the schema.
---
packages/back-end/src/integrations/Mssql.ts | 6 +++---
packages/back-end/src/integrations/Snowflake.ts | 6 +++---
packages/back-end/types/integrations/mssql.d.ts | 1 +
packages/front-end/components/Settings/MssqlForm.tsx | 11 +++++++++++
4 files changed, 18 insertions(+), 6 deletions(-)
diff --git a/packages/back-end/src/integrations/Mssql.ts b/packages/back-end/src/integrations/Mssql.ts
index e154e621e41..82f1f1c4a05 100644
--- a/packages/back-end/src/integrations/Mssql.ts
+++ b/packages/back-end/src/integrations/Mssql.ts
@@ -80,10 +80,10 @@ export default class Mssql extends SqlIntegration {
getEventsTrackedByDatasourceFromClause(
trackedEventTableName: string
): string {
- if (!this.params.database)
+ if (!this.params.defaultSchema)
throw new Error(
- "No database provided. To automatically generate metrics, you must provide a database."
+ "No default schema provided. To automatically generate metrics, you must provide a default schema. This should be the schema where the Rudderstack tracked events are stored."
);
- return `${this.params.database}.${trackedEventTableName}`;
+ return `${this.params.defaultSchema}.${trackedEventTableName}`;
}
}
diff --git a/packages/back-end/src/integrations/Snowflake.ts b/packages/back-end/src/integrations/Snowflake.ts
index b9514fbf44e..a8c6121dcbd 100644
--- a/packages/back-end/src/integrations/Snowflake.ts
+++ b/packages/back-end/src/integrations/Snowflake.ts
@@ -50,10 +50,10 @@ export default class Snowflake extends SqlIntegration {
getEventsTrackedByDatasourceFromClause(
trackedEventTableName: string
): string {
- if (!this.params.database)
+ if (!this.params.schema)
throw new Error(
- "No database provided. To automatically generate metrics, you must provide a database."
+ "No schema provided. To automatically generate metrics, you must provide a schema."
);
- return `${this.params.database}.${trackedEventTableName}`;
+ return `${this.params.schema}.${trackedEventTableName}`;
}
}
diff --git a/packages/back-end/types/integrations/mssql.d.ts b/packages/back-end/types/integrations/mssql.d.ts
index 407c0c66a29..42283405706 100644
--- a/packages/back-end/types/integrations/mssql.d.ts
+++ b/packages/back-end/types/integrations/mssql.d.ts
@@ -4,6 +4,7 @@ export interface MssqlConnectionParams {
database: string;
password: string;
port: number;
+ defaultSchema?: string;
options?: {
encrypt?: boolean; // for azure
trustServerCertificate?: boolean; // change to true for local dev / self-signed certs
diff --git a/packages/front-end/components/Settings/MssqlForm.tsx b/packages/front-end/components/Settings/MssqlForm.tsx
index 8062a0e8371..729d9ae897f 100644
--- a/packages/front-end/components/Settings/MssqlForm.tsx
+++ b/packages/front-end/components/Settings/MssqlForm.tsx
@@ -80,6 +80,17 @@ const MssqlForm: FC<{
placeholder={existing ? "(Keep existing)" : ""}
/>
+
+
+
+
From 5eeed8fae1b26e118d27476d7f89d9ee17fa7dfb Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Fri, 19 May 2023 08:59:50 -0400
Subject: [PATCH 11/39] Refactors the code a bit to use the same method to get
the from clause for both sql queries relating to the auto metrics - the query
to get a list of tracked events and the query for the actual metric.
---
packages/back-end/src/integrations/Athena.ts | 12 ++++++++----
packages/back-end/src/integrations/BigQuery.ts | 14 --------------
packages/back-end/src/integrations/ClickHouse.ts | 9 +++++++++
packages/back-end/src/integrations/Mssql.ts | 6 ++----
packages/back-end/src/integrations/Postgres.ts | 7 +++++++
packages/back-end/src/integrations/Redshift.ts | 13 +++++++++++++
packages/back-end/src/integrations/Snowflake.ts | 16 ++++++++++------
.../back-end/src/integrations/SqlIntegration.ts | 12 +++---------
8 files changed, 52 insertions(+), 37 deletions(-)
diff --git a/packages/back-end/src/integrations/Athena.ts b/packages/back-end/src/integrations/Athena.ts
index fb491278c2a..de714fff851 100644
--- a/packages/back-end/src/integrations/Athena.ts
+++ b/packages/back-end/src/integrations/Athena.ts
@@ -59,13 +59,17 @@ export default class Athena extends SqlIntegration {
getInformationSchemaTableFromClause(databaseName: string): string {
return `${databaseName}.information_schema.columns`;
}
- getEventsTrackedByDatasourceFromClause(
- trackedEventTableName: string
- ): string {
+ getAutoGeneratedMetricFromClause(trackedEventTableName: string): string {
+ // MKTODO: Test if the database is necessary here
+ if (!this.params.database) {
+ throw new MissingDatasourceParamsError(
+ "To automatically generate metrics for an Athena data source, you must define a default database."
+ );
+ }
if (!this.params.catalog)
throw new MissingDatasourceParamsError(
"To automatically generate metrics for an Athena data source, you must define a default catalog."
);
- return `${this.params.catalog}.${trackedEventTableName}`;
+ return `${this.params.database}.${this.params.catalog}.${trackedEventTableName}`;
}
}
diff --git a/packages/back-end/src/integrations/BigQuery.ts b/packages/back-end/src/integrations/BigQuery.ts
index ebb6881ce58..0bc480f6d3b 100644
--- a/packages/back-end/src/integrations/BigQuery.ts
+++ b/packages/back-end/src/integrations/BigQuery.ts
@@ -99,20 +99,6 @@ export default class BigQuery extends SqlIntegration {
): string {
return `\`${databaseName}.${tableSchema}.INFORMATION_SCHEMA.COLUMNS\``;
}
- getEventsTrackedByDatasourceFromClause(
- trackedEventTableName: string
- ): string {
- if (!this.params.projectId)
- throw new Error(
- "No projectId provided. To automatically generate metrics you must provide a projectId."
- );
- if (!this.params.defaultDataset)
- throw new MissingDatasourceParamsError(
- "To automatically generate metrics for a BigQuery dataset, you must define a default dataset."
- );
- return `\`${this.params.projectId}.${this.params.defaultDataset}.${trackedEventTableName}\``;
- }
-
getAutoGeneratedMetricFromClause(event: string): string {
if (!this.params.projectId)
throw new Error(
diff --git a/packages/back-end/src/integrations/ClickHouse.ts b/packages/back-end/src/integrations/ClickHouse.ts
index 858052b91e1..c8754351fb8 100644
--- a/packages/back-end/src/integrations/ClickHouse.ts
+++ b/packages/back-end/src/integrations/ClickHouse.ts
@@ -1,6 +1,7 @@
import { ClickHouse as ClickHouseClient } from "clickhouse";
import { decryptDataSourceParams } from "../services/datasource";
import { ClickHouseConnectionParams } from "../../types/integrations/clickhouse";
+import { MissingDatasourceParamsError } from "../types/Integration";
import SqlIntegration from "./SqlIntegration";
export default class ClickHouse extends SqlIntegration {
@@ -90,4 +91,12 @@ export default class ClickHouse extends SqlIntegration {
);
return `table_schema IN ('${this.params.database}')`;
}
+ getAutoGeneratedMetricFromClause(trackedEventTableName: string): string {
+ if (!this.params.database) {
+ throw new MissingDatasourceParamsError(
+ "No database provided. To automatically generate metrics, you must provide a database."
+ );
+ }
+ return `${this.params.database}.${trackedEventTableName}`;
+ }
}
diff --git a/packages/back-end/src/integrations/Mssql.ts b/packages/back-end/src/integrations/Mssql.ts
index 82f1f1c4a05..6476d4592f5 100644
--- a/packages/back-end/src/integrations/Mssql.ts
+++ b/packages/back-end/src/integrations/Mssql.ts
@@ -77,12 +77,10 @@ export default class Mssql extends SqlIntegration {
getInformationSchemaTableFromClause(databaseName: string): string {
return `${databaseName}.information_schema.columns`;
}
- getEventsTrackedByDatasourceFromClause(
- trackedEventTableName: string
- ): string {
+ getAutoGeneratedMetricFromClause(trackedEventTableName: string): string {
if (!this.params.defaultSchema)
throw new Error(
- "No default schema provided. To automatically generate metrics, you must provide a default schema. This should be the schema where the Rudderstack tracked events are stored."
+ "No default schema provided. To automatically generate metrics, you must provide a default schema. This should be the schema where your tracked events are stored."
);
return `${this.params.defaultSchema}.${trackedEventTableName}`;
}
diff --git a/packages/back-end/src/integrations/Postgres.ts b/packages/back-end/src/integrations/Postgres.ts
index bd52c31316c..5b8f5ed2c4c 100644
--- a/packages/back-end/src/integrations/Postgres.ts
+++ b/packages/back-end/src/integrations/Postgres.ts
@@ -38,4 +38,11 @@ export default class Postgres extends SqlIntegration {
getInformationSchemaWhereClause(): string {
return "table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast')";
}
+ getAutoGeneratedMetricFromClause(trackedEventTableName: string): string {
+ if (!this.params.defaultSchema)
+ throw new Error(
+ "No default schema provided. To automatically generate metrics, you must provide a default schema. This should be the schema where your tracked events are stored."
+ );
+ return `${this.params.defaultSchema}.${trackedEventTableName}`;
+ }
}
diff --git a/packages/back-end/src/integrations/Redshift.ts b/packages/back-end/src/integrations/Redshift.ts
index 7fcbeb1a90f..dda4a8f9450 100644
--- a/packages/back-end/src/integrations/Redshift.ts
+++ b/packages/back-end/src/integrations/Redshift.ts
@@ -1,6 +1,7 @@
import { PostgresConnectionParams } from "../../types/integrations/postgres";
import { decryptDataSourceParams } from "../services/datasource";
import { runPostgresQuery } from "../services/postgres";
+import { MissingDatasourceParamsError } from "../types/Integration";
import { FormatDialect } from "../util/sql";
import SqlIntegration from "./SqlIntegration";
@@ -40,4 +41,16 @@ export default class Redshift extends SqlIntegration {
getInformationSchemaTableFromClause(): string {
return "SVV_COLUMNS";
}
+ getAutoGeneratedMetricFromClause(trackedEventTableName: string): string {
+ if (!this.params.database) {
+ throw new MissingDatasourceParamsError(
+ "To automatically generate metrics for an Athena data source, you must define a default database."
+ );
+ }
+ if (!this.params.defaultSchema)
+ throw new MissingDatasourceParamsError(
+ "To automatically generate metrics for an Athena data source, you must define a default catalog."
+ );
+ return `${this.params.database}.${this.params.defaultSchema}.${trackedEventTableName}`;
+ }
}
diff --git a/packages/back-end/src/integrations/Snowflake.ts b/packages/back-end/src/integrations/Snowflake.ts
index a8c6121dcbd..3ccead1cd50 100644
--- a/packages/back-end/src/integrations/Snowflake.ts
+++ b/packages/back-end/src/integrations/Snowflake.ts
@@ -1,6 +1,7 @@
import { SnowflakeConnectionParams } from "../../types/integrations/snowflake";
import { decryptDataSourceParams } from "../services/datasource";
import { runSnowflakeQuery } from "../services/snowflake";
+import { MissingDatasourceParamsError } from "../types/Integration";
import { FormatDialect } from "../util/sql";
import SqlIntegration from "./SqlIntegration";
@@ -36,7 +37,7 @@ export default class Snowflake extends SqlIntegration {
}
getInformationSchemaFromClause(): string {
if (!this.params.database)
- throw new Error(
+ throw new MissingDatasourceParamsError(
"No database provided. In order to get the information schema, you must provide a database."
);
return `${this.params.database}.information_schema.columns`;
@@ -47,13 +48,16 @@ export default class Snowflake extends SqlIntegration {
getInformationSchemaTableFromClause(databaseName: string): string {
return `${databaseName}.information_schema.columns`;
}
- getEventsTrackedByDatasourceFromClause(
- trackedEventTableName: string
- ): string {
+ getAutoGeneratedMetricFromClause(trackedEventTableName: string): string {
+ if (!this.params.database) {
+ throw new MissingDatasourceParamsError(
+ "No database provided. To automatically generate metrics, you must provide a database."
+ );
+ }
if (!this.params.schema)
- throw new Error(
+ throw new MissingDatasourceParamsError(
"No schema provided. To automatically generate metrics, you must provide a schema."
);
- return `${this.params.schema}.${trackedEventTableName}`;
+ return `${this.params.database}.${this.params.schema}.${trackedEventTableName}`;
}
}
diff --git a/packages/back-end/src/integrations/SqlIntegration.ts b/packages/back-end/src/integrations/SqlIntegration.ts
index 478e8612961..790d6120b26 100644
--- a/packages/back-end/src/integrations/SqlIntegration.ts
+++ b/packages/back-end/src/integrations/SqlIntegration.ts
@@ -1303,10 +1303,8 @@ export default abstract class SqlIntegration
return { tableData };
}
- getEventsTrackedByDatasourceFromClause(
- trackedEventTableName: string
- ): string {
- return trackedEventTableName;
+ getAutoGeneratedMetricFromClause(event: string): string {
+ return event;
}
async getEventsTrackedByDatasource(
@@ -1328,7 +1326,7 @@ export default abstract class SqlIntegration
(CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId,
(CASE WHEN (1 > 0) THEN 1 ELSE 0 END) as createForUser
FROM
- ${this.getEventsTrackedByDatasourceFromClause(trackedEventTableName)}
+ ${this.getAutoGeneratedMetricFromClause(trackedEventTableName)}
GROUP BY event`;
const results = await this.runQuery(format(sql, this.getFormatDialect()));
@@ -1340,10 +1338,6 @@ export default abstract class SqlIntegration
return results;
}
- getAutoGeneratedMetricFromClause(event: string): string {
- return event;
- }
-
getAutoGeneratedMetricTimestampColumn(schemaFormat: SchemaFormat): string {
switch (schemaFormat) {
case "segment" || "rudderstack":
From 6ccfdfe1f12aef034b3140759acfca33aef04717 Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Fri, 19 May 2023 09:19:53 -0400
Subject: [PATCH 12/39] Improves error handling
---
.../back-end/src/controllers/datasources.ts | 16 +++----
.../src/jobs/createAutomaticMetrics.ts | 46 ++++++++++---------
2 files changed, 33 insertions(+), 29 deletions(-)
diff --git a/packages/back-end/src/controllers/datasources.ts b/packages/back-end/src/controllers/datasources.ts
index 4ccc7dfb826..12f957e450c 100644
--- a/packages/back-end/src/controllers/datasources.ts
+++ b/packages/back-end/src/controllers/datasources.ts
@@ -629,8 +629,8 @@ export async function getAutoGeneratedMetricsToCreate(
const dataSourceObj = await getDataSourceById(id, org.id);
if (!dataSourceObj) {
- res.status(403).json({
- status: 403,
+ res.status(404).json({
+ status: 404,
message: "Invalid data source: " + id,
});
return;
@@ -648,9 +648,8 @@ export async function getAutoGeneratedMetricsToCreate(
!integration.settings.schemaFormat ||
!integration.getSourceProperties().supportsAutoGeneratedMetrics
) {
- //TODO: Is this the correct error code?
- res.status(403).json({
- status: 403,
+ res.status(400).json({
+ status: 400,
message: "This datasource does not support automatic metric generation.",
});
return;
@@ -662,11 +661,12 @@ export async function getAutoGeneratedMetricsToCreate(
integration.settings.schemaFormat
);
- if (!results.length) {
+ if (results.length) {
return res.status(200).json({
status: 200,
- results,
- message: "No events found.",
+ results: [],
+ message:
+ "We were unable to identify any metrics to generate automatically for you.",
});
}
diff --git a/packages/back-end/src/jobs/createAutomaticMetrics.ts b/packages/back-end/src/jobs/createAutomaticMetrics.ts
index 1c1a3752d2c..c02005f7d70 100644
--- a/packages/back-end/src/jobs/createAutomaticMetrics.ts
+++ b/packages/back-end/src/jobs/createAutomaticMetrics.ts
@@ -9,6 +9,7 @@ import { getInformationSchemaById } from "../models/InformationSchemaModel";
import { fetchTableData } from "../services/informationSchema";
import { getPath } from "../util/informationSchemas";
import { Column } from "../types/Integration";
+import { logger } from "../util/logger";
const CREATE_AUTOMATIC_METRICS_JOB_NAME = "createAutomaticMetrics";
@@ -32,34 +33,34 @@ export default function (ag: Agenda) {
console.log("made it to the job");
const { datasourceId, organization, metricsToCreate } = job.attrs.data;
- if (!datasourceId || !organization || !metricsToCreate) return;
-
- const datasource = await getDataSourceById(datasourceId, organization);
+ try {
+ const datasource = await getDataSourceById(datasourceId, organization);
- if (!datasource) return;
+ if (!datasource) throw new Error("No datasource");
- const integration = getSourceIntegrationObject(datasource);
+ const integration = getSourceIntegrationObject(datasource);
- if (
- !integration.getAutoGeneratedMetricSqlQuery ||
- !integration.getSourceProperties().supportsAutoGeneratedMetrics
- )
- return;
+ if (
+ !integration.getAutoGeneratedMetricSqlQuery ||
+ !integration.getSourceProperties().supportsAutoGeneratedMetrics
+ )
+ throw new Error(
+ "Auto generated metrics not supported for this data source"
+ );
- const informationSchemaId = datasource.settings.informationSchemaId;
+ const informationSchemaId = datasource.settings.informationSchemaId;
- if (!informationSchemaId) return; //TODO: Throw an error?
+ if (!informationSchemaId) throw new Error("No information schema id");
- const informationSchema = await getInformationSchemaById(
- organization,
- informationSchemaId
- );
+ const informationSchema = await getInformationSchemaById(
+ organization,
+ informationSchemaId
+ );
- if (!informationSchema) return; //TODO: Throw an error?
+ if (!informationSchema) throw new Error("No information schema");
- let informationSchemaTableId = "";
+ let informationSchemaTableId = "";
- try {
const metrics: Partial[] = [];
for (const metric of metricsToCreate) {
if (metric.createForUser) {
@@ -84,7 +85,7 @@ export default function (ag: Agenda) {
informationSchemaTableId
);
- if (!tableData) return; //TODO: Throw an error?
+ if (!tableData) throw new Error("No table data");
const columns: Column[] = tableData?.map(
(row: { column_name: string; data_type: string }) => {
@@ -131,7 +132,10 @@ export default function (ag: Agenda) {
}
await insertMetrics(metrics);
} catch (e) {
- // Not sure what to do here yet - catch the errors, but what should I do with them?
+ logger.error(
+ e,
+ "Failed to generate automatic metrics. Reason: " + e.message
+ );
}
}
);
From d10ef04b9944487208e9dd9356038fbad9aa6375 Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Fri, 19 May 2023 09:51:35 -0400
Subject: [PATCH 13/39] Resets controller logic back after testing, and renames
helper method.
---
packages/back-end/src/controllers/datasources.ts | 4 ++--
packages/back-end/src/integrations/SqlIntegration.ts | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/back-end/src/controllers/datasources.ts b/packages/back-end/src/controllers/datasources.ts
index 12f957e450c..20754a4303f 100644
--- a/packages/back-end/src/controllers/datasources.ts
+++ b/packages/back-end/src/controllers/datasources.ts
@@ -661,10 +661,10 @@ export async function getAutoGeneratedMetricsToCreate(
integration.settings.schemaFormat
);
- if (results.length) {
+ if (!results.length) {
return res.status(200).json({
status: 200,
- results: [],
+ results,
message:
"We were unable to identify any metrics to generate automatically for you.",
});
diff --git a/packages/back-end/src/integrations/SqlIntegration.ts b/packages/back-end/src/integrations/SqlIntegration.ts
index 790d6120b26..ca058dbab96 100644
--- a/packages/back-end/src/integrations/SqlIntegration.ts
+++ b/packages/back-end/src/integrations/SqlIntegration.ts
@@ -80,7 +80,7 @@ export default abstract class SqlIntegration
activationDimension: true,
pastExperiments: true,
supportsInformationSchema: true,
- supportsAutoGeneratedMetrics: this.isAutoGeneratedMetricSupported(),
+ supportsAutoGeneratedMetrics: this.isAutoGeneratingMetricSupported(),
};
}
@@ -89,7 +89,7 @@ export default abstract class SqlIntegration
return true;
}
- isAutoGeneratedMetricSupported(): boolean {
+ isAutoGeneratingMetricSupported(): boolean {
const supportedEventTrackers: SchemaFormat[] = ["segment", "rudderstack"];
if (
From 145b9cd080d2d3d11806ff17d3eb0dc7afa52b48 Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Wed, 24 May 2023 06:49:51 -0400
Subject: [PATCH 14/39] Refactored a bit based on Jeremy's feedback, needs a
lot of cleanup, but committing in, instead of stashing it while I go bash a
bug.
---
.../back-end/src/controllers/datasources.ts | 2 +
packages/back-end/src/integrations/Athena.ts | 29 ++-
.../back-end/src/integrations/BigQuery.ts | 17 +-
packages/back-end/src/integrations/Mssql.ts | 26 +--
packages/back-end/src/integrations/Presto.ts | 9 +-
.../back-end/src/integrations/Redshift.ts | 27 +--
.../back-end/src/integrations/Snowflake.ts | 27 ++-
.../src/integrations/SqlIntegration.ts | 166 ++++++++++++++----
.../src/jobs/createAutomaticMetrics.ts | 15 +-
packages/back-end/types/datasource.d.ts | 8 +
.../components/Settings/NewDataSourceForm.tsx | 8 +-
11 files changed, 224 insertions(+), 110 deletions(-)
diff --git a/packages/back-end/src/controllers/datasources.ts b/packages/back-end/src/controllers/datasources.ts
index 20754a4303f..9cb2c15e69e 100644
--- a/packages/back-end/src/controllers/datasources.ts
+++ b/packages/back-end/src/controllers/datasources.ts
@@ -424,6 +424,8 @@ export async function putDataSource(
event: string;
hasUserId: boolean;
createForUser: boolean;
+ lastTrackedAt: Date;
+ count: number;
}[];
},
{ id: string }
diff --git a/packages/back-end/src/integrations/Athena.ts b/packages/back-end/src/integrations/Athena.ts
index de714fff851..1cf43e22946 100644
--- a/packages/back-end/src/integrations/Athena.ts
+++ b/packages/back-end/src/integrations/Athena.ts
@@ -49,27 +49,24 @@ export default class Athena extends SqlIntegration {
ensureFloat(col: string): string {
return `1.0*${col}`;
}
- getInformationSchemaFromClause(): string {
- if (!this.params.catalog)
- throw new MissingDatasourceParamsError(
- "To view the information schema for an Athena data source, you must define a default catalog. Please add a default catalog by editing the datasource's connection settings."
- );
- return `${this.params.catalog}.information_schema.columns`;
- }
getInformationSchemaTableFromClause(databaseName: string): string {
return `${databaseName}.information_schema.columns`;
}
- getAutoGeneratedMetricFromClause(trackedEventTableName: string): string {
- // MKTODO: Test if the database is necessary here
- if (!this.params.database) {
- throw new MissingDatasourceParamsError(
- "To automatically generate metrics for an Athena data source, you must define a default database."
- );
- }
+ generateTableName(tableName?: string): string {
if (!this.params.catalog)
throw new MissingDatasourceParamsError(
- "To automatically generate metrics for an Athena data source, you must define a default catalog."
+ "To view the information schema for an Athena data source, you must define a default catalog. Please add a default catalog by editing the datasource's connection settings."
);
- return `${this.params.database}.${this.params.catalog}.${trackedEventTableName}`;
+ if (tableName) {
+ // MKTODO: Test if the database is necessary here
+ if (!this.params.database) {
+ throw new MissingDatasourceParamsError(
+ "To automatically generate metrics for an Athena data source, you must define a default database."
+ );
+ }
+ return `${this.params.database}.${this.params.catalog}.${tableName}`;
+ } else {
+ return `${this.params.catalog}.information_schema.columns`;
+ }
}
}
diff --git a/packages/back-end/src/integrations/BigQuery.ts b/packages/back-end/src/integrations/BigQuery.ts
index 0bc480f6d3b..4fb1b474533 100644
--- a/packages/back-end/src/integrations/BigQuery.ts
+++ b/packages/back-end/src/integrations/BigQuery.ts
@@ -82,24 +82,13 @@ export default class BigQuery extends SqlIntegration {
castUserDateCol(column: string): string {
return `CAST(${column} as DATETIME)`;
}
- getInformationSchemaFromClause(): string {
- if (!this.params.projectId)
- throw new Error(
- "No projectId provided. In order to get the information schema, you must provide a projectId."
- );
- if (!this.params.defaultDataset)
- throw new MissingDatasourceParamsError(
- "To view the information schema for a BigQuery dataset, you must define a default dataset. Please add a default dataset by editing the datasource's connection settings."
- );
- return `\`${this.params.projectId}.${this.params.defaultDataset}.INFORMATION_SCHEMA.COLUMNS\``;
- }
getInformationSchemaTableFromClause(
databaseName: string,
tableSchema: string
): string {
return `\`${databaseName}.${tableSchema}.INFORMATION_SCHEMA.COLUMNS\``;
}
- getAutoGeneratedMetricFromClause(event: string): string {
+ generateTableName(event?: string): string {
if (!this.params.projectId)
throw new Error(
"No projectId provided. To automatically generate metrics you must provide a projectId."
@@ -108,6 +97,8 @@ export default class BigQuery extends SqlIntegration {
throw new MissingDatasourceParamsError(
"To automatically generate metrics for a BigQuery dataset, you must define a default dataset."
);
- return `\`${this.params.projectId}.${this.params.defaultDataset}.${event}\``;
+ return `\`${this.params.projectId}.${this.params.defaultDataset}.${
+ event || "INFORMATION_SCHEMA.COLUMNS"
+ }\``;
}
}
diff --git a/packages/back-end/src/integrations/Mssql.ts b/packages/back-end/src/integrations/Mssql.ts
index 6476d4592f5..daa48395d7b 100644
--- a/packages/back-end/src/integrations/Mssql.ts
+++ b/packages/back-end/src/integrations/Mssql.ts
@@ -67,21 +67,23 @@ export default class Mssql extends SqlIntegration {
formatDateTimeString(col: string): string {
return `CONVERT(VARCHAR(25), ${col}, 121)`;
}
- getInformationSchemaFromClause(): string {
- if (!this.params.database)
- throw new MissingDatasourceParamsError(
- "To view the information schema for a MS Sql dataset, you must define a default database. Please add a default database by editing the datasource's connection settings."
- );
- return `${this.params.database}.information_schema.columns`;
- }
getInformationSchemaTableFromClause(databaseName: string): string {
return `${databaseName}.information_schema.columns`;
}
- getAutoGeneratedMetricFromClause(trackedEventTableName: string): string {
- if (!this.params.defaultSchema)
- throw new Error(
- "No default schema provided. To automatically generate metrics, you must provide a default schema. This should be the schema where your tracked events are stored."
+ generateTableName(tableName?: string): string {
+ if (!this.params.database)
+ throw new MissingDatasourceParamsError(
+ "To view the information schema for a MS Sql dataset, you must define a default database. Please add a default database by editing the datasource's connection settings."
);
- return `${this.params.defaultSchema}.${trackedEventTableName}`;
+ if (tableName) {
+ //MK TODO: Validate if the defaultSchema is necessary here
+ if (!this.params.defaultSchema)
+ throw new Error(
+ "No default schema provided. To automatically generate metrics, you must provide a default schema. This should be the schema where your tracked events are stored."
+ );
+ return `${this.params.defaultSchema}.${tableName}`;
+ } else {
+ return `${this.params.database}.information_schema.columns`;
+ }
}
}
diff --git a/packages/back-end/src/integrations/Presto.ts b/packages/back-end/src/integrations/Presto.ts
index d9a6fb65d49..2a931d2f508 100644
--- a/packages/back-end/src/integrations/Presto.ts
+++ b/packages/back-end/src/integrations/Presto.ts
@@ -106,14 +106,15 @@ export default class Presto extends SqlIntegration {
ensureFloat(col: string): string {
return `CAST(${col} AS DOUBLE)`;
}
- getInformationSchemaFromClause(): string {
+ getInformationSchemaTableFromClause(databaseName: string): string {
+ return `${databaseName}.information_schema.columns`;
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ generateTableName(tableName?: string) {
if (!this.params.catalog)
throw new MissingDatasourceParamsError(
"To view the information schema for a Presto data source, you must define a default catalog. Please add a default catalog by editing the datasource's connection settings."
);
return `${this.params.catalog}.information_schema.columns`;
}
- getInformationSchemaTableFromClause(databaseName: string): string {
- return `${databaseName}.information_schema.columns`;
- }
}
diff --git a/packages/back-end/src/integrations/Redshift.ts b/packages/back-end/src/integrations/Redshift.ts
index dda4a8f9450..1f52d9ea4f0 100644
--- a/packages/back-end/src/integrations/Redshift.ts
+++ b/packages/back-end/src/integrations/Redshift.ts
@@ -35,22 +35,23 @@ export default class Redshift extends SqlIntegration {
ensureFloat(col: string): string {
return `${col}::float`;
}
- getInformationSchemaFromClause(): string {
- return "SVV_COLUMNS";
- }
getInformationSchemaTableFromClause(): string {
return "SVV_COLUMNS";
}
- getAutoGeneratedMetricFromClause(trackedEventTableName: string): string {
- if (!this.params.database) {
- throw new MissingDatasourceParamsError(
- "To automatically generate metrics for an Athena data source, you must define a default database."
- );
+ generateTableName(tableName?: string): string {
+ if (tableName) {
+ if (!this.params.database) {
+ throw new MissingDatasourceParamsError(
+ "To automatically generate metrics for an Athena data source, you must define a default database."
+ );
+ }
+ if (!this.params.defaultSchema)
+ throw new MissingDatasourceParamsError(
+ "To automatically generate metrics for an Athena data source, you must define a default catalog."
+ );
+ return `${this.params.database}.${this.params.defaultSchema}.${tableName}`;
+ } else {
+ return "SVV_COLUMNS";
}
- if (!this.params.defaultSchema)
- throw new MissingDatasourceParamsError(
- "To automatically generate metrics for an Athena data source, you must define a default catalog."
- );
- return `${this.params.database}.${this.params.defaultSchema}.${trackedEventTableName}`;
}
}
diff --git a/packages/back-end/src/integrations/Snowflake.ts b/packages/back-end/src/integrations/Snowflake.ts
index 3ccead1cd50..1944120fb7c 100644
--- a/packages/back-end/src/integrations/Snowflake.ts
+++ b/packages/back-end/src/integrations/Snowflake.ts
@@ -35,29 +35,26 @@ export default class Snowflake extends SqlIntegration {
ensureFloat(col: string): string {
return `CAST(${col} AS DOUBLE)`;
}
- getInformationSchemaFromClause(): string {
- if (!this.params.database)
- throw new MissingDatasourceParamsError(
- "No database provided. In order to get the information schema, you must provide a database."
- );
- return `${this.params.database}.information_schema.columns`;
- }
getInformationSchemaWhereClause(): string {
return "table_schema NOT IN ('INFORMATION_SCHEMA')";
}
getInformationSchemaTableFromClause(databaseName: string): string {
return `${databaseName}.information_schema.columns`;
}
- getAutoGeneratedMetricFromClause(trackedEventTableName: string): string {
- if (!this.params.database) {
+ generateTableName(tableName?: string): string {
+ if (!this.params.database)
throw new MissingDatasourceParamsError(
- "No database provided. To automatically generate metrics, you must provide a database."
+ "No database provided. In order to get the information schema, you must provide a database."
);
+
+ if (tableName) {
+ if (!this.params.schema)
+ throw new MissingDatasourceParamsError(
+ "No schema provided. To automatically generate metrics, you must provide a schema."
+ );
+ return `${this.params.database}.${this.params.schema}.${tableName}`;
+ } else {
+ return `${this.params.database}.information_schema.columns`;
}
- if (!this.params.schema)
- throw new MissingDatasourceParamsError(
- "No schema provided. To automatically generate metrics, you must provide a schema."
- );
- return `${this.params.database}.${this.params.schema}.${trackedEventTableName}`;
}
}
diff --git a/packages/back-end/src/integrations/SqlIntegration.ts b/packages/back-end/src/integrations/SqlIntegration.ts
index ca058dbab96..e13638d7a5f 100644
--- a/packages/back-end/src/integrations/SqlIntegration.ts
+++ b/packages/back-end/src/integrations/SqlIntegration.ts
@@ -8,6 +8,7 @@ import {
ExposureQuery,
DataSourceType,
SchemaFormat,
+ SchemaFormatConfig,
} from "../../types/datasource";
import {
MetricValueParams,
@@ -1245,9 +1246,11 @@ export default abstract class SqlIntegration
async getExperimentResults(): Promise {
throw new Error("Not implemented");
}
- getInformationSchemaFromClause(): string {
- return "information_schema.columns";
+
+ generateTableName(tableName?: string): string {
+ return tableName || "information_schema.columns";
}
+
getInformationSchemaWhereClause(): string {
return "table_schema NOT IN ('information_schema')";
}
@@ -1267,7 +1270,7 @@ export default abstract class SqlIntegration
table_schema as table_schema,
count(column_name) as column_count
FROM
- ${this.getInformationSchemaFromClause()}
+ ${this.generateTableName()}
WHERE ${this.getInformationSchemaWhereClause()}
GROUP BY table_name, table_schema, table_catalog`;
@@ -1303,50 +1306,140 @@ export default abstract class SqlIntegration
return { tableData };
}
- getAutoGeneratedMetricFromClause(event: string): string {
- return event;
+ getSchemaFormatConfig(schemaFormat: SchemaFormat): SchemaFormatConfig {
+ switch (schemaFormat) {
+ // case "amplitude": TODO: Add support for amplitude later
+ // return {
+ // trackedEventTableName: "tracks",
+ // eventColumn: "event",
+ // timestampColumn: "server_upload_time",
+ // userIdColumn: "user_id",
+ // anonymousIdColumn: "amplitude_id",
+ // };
+ // Segment & Rudderstack
+ default:
+ return {
+ trackedEventTableName: "tracks",
+ eventColumn: "event",
+ timestampColumn: "received_at",
+ userIdColumn: "user_id",
+ anonymousIdColumn: "anonymous_id",
+ };
+ }
+ }
+
+ toTitleCase(s: string): string {
+ return s
+ .replace(/^[-_]*(.)/, (_, c) => c.toUpperCase())
+ .replace(/[-_]+(.)/g, (_, c) => " " + c.toUpperCase());
}
async getEventsTrackedByDatasource(
schemaFormat: SchemaFormat
): Promise<{ event: string; hasUserId: boolean; createForUser: boolean }[]> {
- let eventColumn;
- let trackedEventTableName;
+ const {
+ trackedEventTableName,
+ eventColumn,
+ timestampColumn,
+ } = this.getSchemaFormatConfig(schemaFormat);
+
+ //MKTODO: Refactor how we're building the date string, it needs to be in the YYYY-mm-dd format
+ //MKTODO: Verify this sql works for non big query tables (it worked for MsSql & BigQuery)
+ // Create a date object from a date string
+ const currentDateTime = new Date();
+ const sevenDaysAgo = new Date(
+ currentDateTime.valueOf() - 7 * 60 * 60 * 24 * 1000
+ );
- switch (schemaFormat) {
- //TODO: Add cases for schemaFormats where the tracked events aren't stored in a column called "events"
- default:
- eventColumn = "event";
- trackedEventTableName = "tracks";
- }
+ // Generate yyyy-mm-dd date string
+ const currentDateTimeString =
+ "'" +
+ currentDateTime.toLocaleString("default", { year: "numeric" }) +
+ "-" +
+ currentDateTime.toLocaleString("default", {
+ month: "2-digit",
+ }) +
+ "-" +
+ currentDateTime.toLocaleString("default", { day: "2-digit" }) +
+ "'";
+
+ // Generate yyyy-mm-dd date string
+ const sevenDaysAgoDateTimeString =
+ "'" +
+ sevenDaysAgo.toLocaleString("default", { year: "numeric" }) +
+ "-" +
+ sevenDaysAgo.toLocaleString("default", {
+ month: "2-digit",
+ }) +
+ "-" +
+ sevenDaysAgo.toLocaleString("default", { day: "2-digit" }) +
+ "'";
const sql = `
SELECT
${eventColumn} as event,
+ ${timestampColumn},
(CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId,
- (CASE WHEN (1 > 0) THEN 1 ELSE 0 END) as createForUser
+ COUNT (*) as count,
+ MAX(${timestampColumn}) as lastTrackedAt
FROM
- ${this.getAutoGeneratedMetricFromClause(trackedEventTableName)}
- GROUP BY event`;
+ ${this.generateTableName(trackedEventTableName)}
+ WHERE received_at > ${sevenDaysAgoDateTimeString} AND received_at < ${currentDateTimeString}
+ AND event NOT IN ('experiment_viewed', 'experiment_started')
+ GROUP BY event, ${timestampColumn}`;
+
+ const pageViewedSql = `
+ SELECT
+ context_page_title as event,
+ ${timestampColumn},
+ (CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId,
+ COUNT (*) as count,
+ MAX(${timestampColumn}) as lastTrackedAt
+ FROM
+ ${this.generateTableName("pages")}
+ GROUP BY event, ${timestampColumn}
+ ORDER BY lastTrackedAt DESC
+ LIMIT 1`;
const results = await this.runQuery(format(sql, this.getFormatDialect()));
- if (!results) {
+ const pageViewedResults = await this.runQuery(
+ format(pageViewedSql, this.getFormatDialect())
+ );
+
+ console.log("results: ", results);
+
+ if (!results && !pageViewedResults) {
throw new Error(`No events found.`);
}
- return results;
- }
+ const formattedResults: {
+ event: string;
+ displayName: string;
+ createForUser: boolean;
+ lastTrackedAt: Date;
+ hasUserId: boolean;
+ count: number;
+ }[] = [];
+
+ results.forEach((result) => {
+ result.createForUser = true;
+ result.lastTrackedAt = new Date(result.lastTrackedAt.value);
+ result.displayName =
+ schemaFormat === "segment"
+ ? this.toTitleCase(result.event)
+ : result.event;
+ formattedResults.push(result);
+ });
- getAutoGeneratedMetricTimestampColumn(schemaFormat: SchemaFormat): string {
- switch (schemaFormat) {
- case "segment" || "rudderstack":
- return "received_at";
- case "amplitude":
- return "server_upload_time";
- default:
- return "timestamp";
+ if (pageViewedResults.length) {
+ pageViewedResults[0].event = "pages";
+ pageViewedResults[0].displayName = "Page Viewed";
+ pageViewedResults[0].createForUser = true;
+ formattedResults.push(pageViewedResults[0]);
}
+
+ return formattedResults;
}
getAutoGeneratedMetricValueColumn(
@@ -1368,23 +1461,30 @@ export default abstract class SqlIntegration
getAutoGeneratedMetricSqlQuery(
metric: {
event: string;
+ displayName: string;
hasUserId: boolean;
createForUser: boolean;
+ lastTrackedAt: Date;
+ count: number;
},
schemaFormat: SchemaFormat,
metricType: MetricType
): string {
+ const {
+ timestampColumn,
+ userIdColumn,
+ anonymousIdColumn,
+ } = this.getSchemaFormatConfig(schemaFormat);
+
const sqlQuery = `
SELECT
- ${metric.hasUserId ? "user_id, " : ""}${
- schemaFormat === "amplitude" ? "amplitude_id" : "anonymous_id"
- } as anonymous_id, ${this.getAutoGeneratedMetricTimestampColumn(
- schemaFormat
- )} as timestamp
+ ${
+ metric.hasUserId ? `${userIdColumn}, ` : ""
+ }${anonymousIdColumn} as anonymous_id, ${timestampColumn} as timestamp
${this.getAutoGeneratedMetricValueColumn(
metricType,
schemaFormat
- )} FROM ${this.getAutoGeneratedMetricFromClause(metric.event)}
+ )} FROM ${this.generateTableName(metric.event)}
`;
return format(sqlQuery, this.getFormatDialect());
diff --git a/packages/back-end/src/jobs/createAutomaticMetrics.ts b/packages/back-end/src/jobs/createAutomaticMetrics.ts
index c02005f7d70..1bc4cdd8063 100644
--- a/packages/back-end/src/jobs/createAutomaticMetrics.ts
+++ b/packages/back-end/src/jobs/createAutomaticMetrics.ts
@@ -20,9 +20,16 @@ type CreateAutomaticMetricsJob = Job<{
event: string;
hasUserId: boolean;
createForUser: boolean;
+ displayName: string;
+ lastTrackedAt: Date;
+ count: number;
}[];
}>;
+function toSnakeCase(s: string): string {
+ return s.replace(" ", "_").toLowerCase();
+}
+
let agenda: Agenda;
export default function (ag: Agenda) {
agenda = ag;
@@ -30,7 +37,6 @@ export default function (ag: Agenda) {
agenda.define(
CREATE_AUTOMATIC_METRICS_JOB_NAME,
async (job: CreateAutomaticMetricsJob) => {
- console.log("made it to the job");
const { datasourceId, organization, metricsToCreate } = job.attrs.data;
try {
@@ -67,7 +73,10 @@ export default function (ag: Agenda) {
informationSchema.databases.forEach((database) => {
database.schemas.forEach((schema) => {
schema.tables.forEach((table) => {
- if (table.tableName === metric.event) {
+ if (
+ table.tableName === metric.event ||
+ table.tableName === toSnakeCase(metric.event)
+ ) {
informationSchemaTableId = table.id;
}
});
@@ -122,7 +131,7 @@ export default function (ag: Agenda) {
id: uniqid("met_"),
organization,
datasource: datasourceId,
- name: metric.event,
+ name: metric.displayName,
type: metricType,
sql: sqlQuery,
dateCreated: new Date(),
diff --git a/packages/back-end/types/datasource.d.ts b/packages/back-end/types/datasource.d.ts
index 64fffc0ebef..54fbe41b95c 100644
--- a/packages/back-end/types/datasource.d.ts
+++ b/packages/back-end/types/datasource.d.ts
@@ -81,6 +81,14 @@ export interface SchemaInterface {
getMetricSQL(name: string, type: MetricType, tablePrefix: string): string;
}
+export interface SchemaFormatConfig {
+ userIdColumn: string;
+ anonymousIdColumn: string;
+ trackedEventTableName: string;
+ eventColumn: string;
+ timestampColumn: string;
+}
+
export interface DataSourceProperties {
queryLanguage: QueryLanguage;
metricCaps?: boolean;
diff --git a/packages/front-end/components/Settings/NewDataSourceForm.tsx b/packages/front-end/components/Settings/NewDataSourceForm.tsx
index c34f4316f98..7eddfc8255e 100644
--- a/packages/front-end/components/Settings/NewDataSourceForm.tsx
+++ b/packages/front-end/components/Settings/NewDataSourceForm.tsx
@@ -70,6 +70,9 @@ const NewDataSourceForm: FC<{
event: string;
hasUserId: boolean;
createForUser: boolean;
+ displayName: string;
+ lastTrackedAt: Date;
+ count: number;
}[]
>([]);
@@ -577,8 +580,11 @@ const NewDataSourceForm: FC<{
const res = await apiCall<{
results: {
event: string;
+ displayName: string;
hasUserId: boolean;
createForUser: boolean;
+ lastTrackedAt: Date;
+ count: number;
}[];
message?: string;
}>(`/datasource/${dataSourceId}/auto-metrics`, {
@@ -620,7 +626,7 @@ const NewDataSourceForm: FC<{
setMetricsToCreate(newMetricsToCreate);
}}
/>
-
+
);
})}
From 08c37e143c1adc3075d01b2f5def50bfec92f377 Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Wed, 24 May 2023 14:33:25 -0400
Subject: [PATCH 15/39] Starts scaffolding out how we can build additional
metrics when retreiving the list of tracked events.
---
.../src/integrations/SqlIntegration.ts | 188 ++++++++++--------
.../src/jobs/createAutomaticMetrics.ts | 89 +++++----
packages/back-end/src/types/Integration.ts | 3 +-
3 files changed, 152 insertions(+), 128 deletions(-)
diff --git a/packages/back-end/src/integrations/SqlIntegration.ts b/packages/back-end/src/integrations/SqlIntegration.ts
index e13638d7a5f..a402161c7a1 100644
--- a/packages/back-end/src/integrations/SqlIntegration.ts
+++ b/packages/back-end/src/integrations/SqlIntegration.ts
@@ -1328,10 +1328,63 @@ export default abstract class SqlIntegration
}
}
- toTitleCase(s: string): string {
- return s
- .replace(/^[-_]*(.)/, (_, c) => c.toUpperCase())
- .replace(/[-_]+(.)/g, (_, c) => " " + c.toUpperCase());
+ formatTrackedEventResults(results: any[], schemaFormat: SchemaFormat): any[] {
+ function toTitleCase(input: string): string {
+ return input
+ .replace(/^[-_]*(.)/, (_, c) => c.toUpperCase())
+ .replace(/[-_]+(.)/g, (_, c) => " " + c.toUpperCase());
+ }
+
+ const formattedResults: {
+ event: string;
+ displayName: string;
+ createForUser: boolean;
+ lastTrackedAt: Date;
+ hasUserId: boolean;
+ count: number;
+ }[] = [];
+
+ results.forEach((result) => {
+ result.createForUser = true;
+ result.lastTrackedAt = new Date(result.lastTrackedAt.value);
+ result.displayName = result.event;
+ result.type = "binomial";
+
+ if (schemaFormat === "segment") {
+ result.displayName = toTitleCase(result.event);
+ }
+ formattedResults.push(result);
+ });
+
+ return formattedResults;
+ }
+
+ generateMetricsToCreate(
+ formattedResults: any[],
+ timestampColumn: string,
+ userIdColumn: string,
+ anonymousIdColumn: string
+ ): any[] {
+ const metricsToCreate = cloneDeep(formattedResults);
+ // What are we trying to do here? Let's say formattedResults has 4 metrics - Placed Order, Added to Cart, Viewed Product, Subscribed, & Page Viewed
+ formattedResults.forEach((result) => {
+ // Do something here
+ if (result.event === "placed_order") {
+ const metric = {
+ event: result.event,
+ displayName: "Average Order Value",
+ createForUser: true,
+ lastTrackedAt: undefined,
+ hasUserId: result.hasUserId,
+ type: "revenue",
+ valueClause: "AVG(revenue)",
+ groupByClause: `GROUP BY ${userIdColumn}, ${anonymousIdColumn}, ${timestampColumn}`,
+ };
+ metricsToCreate.push(metric);
+ }
+ });
+
+ return metricsToCreate;
}
async getEventsTrackedByDatasource(
@@ -1341,40 +1394,15 @@ export default abstract class SqlIntegration
trackedEventTableName,
eventColumn,
timestampColumn,
+ userIdColumn,
+ anonymousIdColumn,
} = this.getSchemaFormatConfig(schemaFormat);
- //MKTODO: Refactor how we're building the date string, it needs to be in the YYYY-mm-dd format
- //MKTODO: Verify this sql works for non big query tables (it worked for MsSql & BigQuery)
- // Create a date object from a date string
const currentDateTime = new Date();
const sevenDaysAgo = new Date(
currentDateTime.valueOf() - 7 * 60 * 60 * 24 * 1000
);
- // Generate yyyy-mm-dd date string
- const currentDateTimeString =
- "'" +
- currentDateTime.toLocaleString("default", { year: "numeric" }) +
- "-" +
- currentDateTime.toLocaleString("default", {
- month: "2-digit",
- }) +
- "-" +
- currentDateTime.toLocaleString("default", { day: "2-digit" }) +
- "'";
-
- // Generate yyyy-mm-dd date string
- const sevenDaysAgoDateTimeString =
- "'" +
- sevenDaysAgo.toLocaleString("default", { year: "numeric" }) +
- "-" +
- sevenDaysAgo.toLocaleString("default", {
- month: "2-digit",
- }) +
- "-" +
- sevenDaysAgo.toLocaleString("default", { day: "2-digit" }) +
- "'";
-
const sql = `
SELECT
${eventColumn} as event,
@@ -1384,62 +1412,53 @@ export default abstract class SqlIntegration
MAX(${timestampColumn}) as lastTrackedAt
FROM
${this.generateTableName(trackedEventTableName)}
- WHERE received_at > ${sevenDaysAgoDateTimeString} AND received_at < ${currentDateTimeString}
+ WHERE received_at < '${currentDateTime
+ .toISOString()
+ .slice(0, 10)}' AND received_at > '${sevenDaysAgo
+ .toISOString()
+ .slice(0, 10)}'
AND event NOT IN ('experiment_viewed', 'experiment_started')
GROUP BY event, ${timestampColumn}`;
- const pageViewedSql = `
- SELECT
- context_page_title as event,
- ${timestampColumn},
- (CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId,
- COUNT (*) as count,
- MAX(${timestampColumn}) as lastTrackedAt
- FROM
- ${this.generateTableName("pages")}
- GROUP BY event, ${timestampColumn}
- ORDER BY lastTrackedAt DESC
- LIMIT 1`;
-
const results = await this.runQuery(format(sql, this.getFormatDialect()));
- const pageViewedResults = await this.runQuery(
- format(pageViewedSql, this.getFormatDialect())
- );
+ if (schemaFormat === "segment") {
+ const pageViewedSql = `
+ SELECT
+ 'page-viewed' as event,
+ ${timestampColumn},
+ (CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId,
+ COUNT (*) as count,
+ MAX(${timestampColumn}) as lastTrackedAt
+ FROM
+ ${this.generateTableName("pages")}
+ GROUP BY event, ${timestampColumn}
+ ORDER BY lastTrackedAt DESC
+ LIMIT 1`;
- console.log("results: ", results);
+ const pageViewedResults = await this.runQuery(
+ format(pageViewedSql, this.getFormatDialect())
+ );
- if (!results && !pageViewedResults) {
- throw new Error(`No events found.`);
+ pageViewedResults.forEach((result) => results.push(result));
}
- const formattedResults: {
- event: string;
- displayName: string;
- createForUser: boolean;
- lastTrackedAt: Date;
- hasUserId: boolean;
- count: number;
- }[] = [];
-
- results.forEach((result) => {
- result.createForUser = true;
- result.lastTrackedAt = new Date(result.lastTrackedAt.value);
- result.displayName =
- schemaFormat === "segment"
- ? this.toTitleCase(result.event)
- : result.event;
- formattedResults.push(result);
- });
-
- if (pageViewedResults.length) {
- pageViewedResults[0].event = "pages";
- pageViewedResults[0].displayName = "Page Viewed";
- pageViewedResults[0].createForUser = true;
- formattedResults.push(pageViewedResults[0]);
+ if (!results) {
+ throw new Error(`No events found.`);
}
- return formattedResults;
+ const formattedResults = this.formatTrackedEventResults(
+ results,
+ schemaFormat
+ );
+
+ // Now that we have the formattedResults, is there where we should build logic to define additional metrics?
+ return this.generateMetricsToCreate(
+ formattedResults,
+ userIdColumn,
+ timestampColumn,
+ anonymousIdColumn
+ );
}
getAutoGeneratedMetricValueColumn(
@@ -1466,9 +1485,11 @@ export default abstract class SqlIntegration
createForUser: boolean;
lastTrackedAt: Date;
count: number;
+ valueClause?: string;
+ type?: MetricType;
+ groupByClause?: string;
},
- schemaFormat: SchemaFormat,
- metricType: MetricType
+ schemaFormat: SchemaFormat
): string {
const {
timestampColumn,
@@ -1481,10 +1502,13 @@ export default abstract class SqlIntegration
${
metric.hasUserId ? `${userIdColumn}, ` : ""
}${anonymousIdColumn} as anonymous_id, ${timestampColumn} as timestamp
- ${this.getAutoGeneratedMetricValueColumn(
- metricType,
- schemaFormat
- )} FROM ${this.generateTableName(metric.event)}
+ ${
+ metric.type !== "binomial" && metric.valueClause
+ ? `, ${metric.valueClause} as value`
+ : ""
+ }
+ FROM ${this.generateTableName(metric.event)}
+ ${metric.groupByClause || ""}
`;
return format(sqlQuery, this.getFormatDialect());
diff --git a/packages/back-end/src/jobs/createAutomaticMetrics.ts b/packages/back-end/src/jobs/createAutomaticMetrics.ts
index 1bc4cdd8063..d7239488ba6 100644
--- a/packages/back-end/src/jobs/createAutomaticMetrics.ts
+++ b/packages/back-end/src/jobs/createAutomaticMetrics.ts
@@ -6,9 +6,6 @@ import { insertMetrics } from "../models/MetricModel";
import { MetricInterface, MetricType } from "../../types/metric";
import { getSourceIntegrationObject } from "../services/datasource";
import { getInformationSchemaById } from "../models/InformationSchemaModel";
-import { fetchTableData } from "../services/informationSchema";
-import { getPath } from "../util/informationSchemas";
-import { Column } from "../types/Integration";
import { logger } from "../util/logger";
const CREATE_AUTOMATIC_METRICS_JOB_NAME = "createAutomaticMetrics";
@@ -23,6 +20,9 @@ type CreateAutomaticMetricsJob = Job<{
displayName: string;
lastTrackedAt: Date;
count: number;
+ type: MetricType;
+ valueClause: string;
+ groupByClause: string;
}[];
}>;
@@ -83,49 +83,50 @@ export default function (ag: Agenda) {
});
});
- const {
- tableData,
- databaseName,
- tableSchema,
- tableName,
- } = await fetchTableData(
- datasource,
- informationSchema,
- informationSchemaTableId
- );
-
- if (!tableData) throw new Error("No table data");
-
- const columns: Column[] = tableData?.map(
- (row: { column_name: string; data_type: string }) => {
- return {
- columnName: row.column_name,
- dataType: row.data_type,
- path: getPath(datasource.type, {
- tableCatalog: databaseName,
- tableSchema: tableSchema,
- tableName: tableName,
- columnName: row.column_name,
- }),
- };
- }
- );
-
- let metricType: MetricType = "binomial";
-
- if (columns.length) {
- if (columns.some((column) => column.columnName === "revenue")) {
- metricType = "revenue";
- } else if (
- columns.some((column) => column.columnName === "count")
- ) {
- metricType = "count";
- }
- }
+ // const {
+ // tableData,
+ // databaseName,
+ // tableSchema,
+ // tableName,
+ // } = await fetchTableData(
+ // datasource,
+ // informationSchema,
+ // informationSchemaTableId
+ // );
+
+ // if (!tableData) throw new Error("No table data");
+
+ // const columns: Column[] = tableData?.map(
+ // (row: { column_name: string; data_type: string }) => {
+ // return {
+ // columnName: row.column_name,
+ // dataType: row.data_type,
+ // path: getPath(datasource.type, {
+ // tableCatalog: databaseName,
+ // tableSchema: tableSchema,
+ // tableName: tableName,
+ // columnName: row.column_name,
+ // }),
+ // };
+ // }
+ // );
+
+ const metricType = metric.type;
+
+ // let metricType: MetricType = "binomial";
+
+ // if (columns.length) {
+ // if (columns.some((column) => column.columnName === "revenue")) {
+ // metricType = "revenue";
+ // } else if (
+ // columns.some((column) => column.columnName === "count")
+ // ) {
+ // metricType = "count";
+ // }
+ // }
const sqlQuery = integration.getAutoGeneratedMetricSqlQuery(
metric,
- integration.settings.schemaFormat || "custom",
- metricType
+ integration.settings.schemaFormat || "custom"
);
metrics.push({
id: uniqid("met_"),
diff --git a/packages/back-end/src/types/Integration.ts b/packages/back-end/src/types/Integration.ts
index 48e8ead6bc1..a3aac48d5ea 100644
--- a/packages/back-end/src/types/Integration.ts
+++ b/packages/back-end/src/types/Integration.ts
@@ -302,7 +302,6 @@ export interface SourceIntegrationInterface {
hasUserId: boolean;
createForUser: boolean;
},
- schemaFormat: SchemaFormat,
- metricType: MetricType
+ schemaFormat: SchemaFormat
): string;
}
From 63d97887f50a7f812c318af7708f008d4d8b653e Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Wed, 24 May 2023 16:21:28 -0400
Subject: [PATCH 16/39] Adds better typing and refactors logic to simplify some
things, while also adding hooks to generate additional metrics from single
tracked event. Very narrow in scope for now, but getting the building blocks
in place.
---
.../back-end/src/controllers/datasources.ts | 3 +-
.../src/integrations/SqlIntegration.ts | 101 +++++++++---------
.../src/jobs/createAutomaticMetrics.ts | 41 +++----
packages/back-end/src/types/Integration.ts | 20 ++--
4 files changed, 81 insertions(+), 84 deletions(-)
diff --git a/packages/back-end/src/controllers/datasources.ts b/packages/back-end/src/controllers/datasources.ts
index 9cb2c15e69e..74f807b2b8d 100644
--- a/packages/back-end/src/controllers/datasources.ts
+++ b/packages/back-end/src/controllers/datasources.ts
@@ -46,6 +46,7 @@ import { EventAuditUserForResponseLocals } from "../events/event-types";
import { deleteInformationSchemaById } from "../models/InformationSchemaModel";
import { deleteInformationSchemaTablesByInformationSchemaId } from "../models/InformationSchemaTablesModel";
import { queueCreateAutomaticMetrics } from "../jobs/createAutomaticMetrics";
+import { AutoGeneratedMetric } from "../types/Integration";
export async function postSampleData(
req: AuthRequest,
@@ -659,7 +660,7 @@ export async function getAutoGeneratedMetricsToCreate(
try {
// Get the list of events tracked by this datasource - we can create metrics from these events.
- const results = await integration.getEventsTrackedByDatasource(
+ const results: AutoGeneratedMetric[] = await integration.getEventsTrackedByDatasource(
integration.settings.schemaFormat
);
diff --git a/packages/back-end/src/integrations/SqlIntegration.ts b/packages/back-end/src/integrations/SqlIntegration.ts
index a402161c7a1..6ea750dd4d3 100644
--- a/packages/back-end/src/integrations/SqlIntegration.ts
+++ b/packages/back-end/src/integrations/SqlIntegration.ts
@@ -25,6 +25,7 @@ import {
MetricAggregationType,
InformationSchema,
RawInformationSchema,
+ AutoGeneratedMetric,
} from "../types/Integration";
import { ExperimentPhase, ExperimentInterface } from "../../types/experiment";
import { DimensionInterface } from "../../types/dimension";
@@ -1328,30 +1329,32 @@ export default abstract class SqlIntegration
}
}
- formatTrackedEventResults(results: any[], schemaFormat: SchemaFormat): any[] {
+ formatTrackedEventResults(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ results: any[],
+ schemaFormat: SchemaFormat
+ ): Omit[] {
function toTitleCase(input: string): string {
return input
.replace(/^[-_]*(.)/, (_, c) => c.toUpperCase())
.replace(/[-_]+(.)/g, (_, c) => " " + c.toUpperCase());
}
- const formattedResults: {
- event: string;
- displayName: string;
- createForUser: boolean;
- lastTrackedAt: Date;
- hasUserId: boolean;
- count: number;
- }[] = [];
+ const formattedResults: Omit<
+ AutoGeneratedMetric,
+ "type" | "valueClause" | "groupByClause"
+ >[] = [];
results.forEach((result) => {
+ result.event;
result.createForUser = true;
result.lastTrackedAt = new Date(result.lastTrackedAt.value);
result.displayName = result.event;
- result.type = "binomial";
+ result.count;
if (schemaFormat === "segment") {
- result.displayName = toTitleCase(result.event);
+ result.displayName =
+ result.event === "pages" ? "Paged Viewed" : toTitleCase(result.event);
}
formattedResults.push(result);
});
@@ -1360,27 +1363,32 @@ export default abstract class SqlIntegration
}
generateMetricsToCreate(
- formattedResults: any[],
+ formattedResults: Omit<
+ AutoGeneratedMetric,
+ "type" | "valueClause" | "groupByClause"
+ >[],
timestampColumn: string,
userIdColumn: string,
anonymousIdColumn: string
- ): any[] {
- const metricsToCreate = cloneDeep(formattedResults);
- // What are we trying to do here? Let's say formattedResults has 4 metrics - Placed Order, Added to Cart, Viewed Product, Subscribed, & Page Viewed
+ ): AutoGeneratedMetric[] {
+ const metricsToCreate: AutoGeneratedMetric[] = [];
formattedResults.forEach((result) => {
- // Do something here
+ const metric: AutoGeneratedMetric = {
+ event: result.event,
+ createForUser: true,
+ lastTrackedAt: result.lastTrackedAt,
+ hasUserId: !!result.hasUserId,
+ count: result.count,
+ displayName: result.displayName,
+ };
+ metricsToCreate.push(metric);
if (result.event === "placed_order") {
- const metric = {
- event: result.event,
- displayName: "Average Order Value",
- createForUser: true,
- lastTrackedAt: undefined,
- hasUserId: result.hasUserId,
- type: "revenue",
- valueClause: "AVG(revenue)",
- groupByClause: `GROUP BY ${userIdColumn}, ${anonymousIdColumn}, ${timestampColumn}`,
- };
- metricsToCreate.push(metric);
+ const newMetric = cloneDeep(metric);
+ newMetric.displayName = "Average Order Value";
+ newMetric.type = "revenue";
+ newMetric.valueClause = "AVG(revenue)";
+ newMetric.groupByClause = `GROUP BY ${userIdColumn}, ${anonymousIdColumn}, ${timestampColumn}`;
+ metricsToCreate.push(newMetric);
}
});
@@ -1389,7 +1397,7 @@ export default abstract class SqlIntegration
async getEventsTrackedByDatasource(
schemaFormat: SchemaFormat
- ): Promise<{ event: string; hasUserId: boolean; createForUser: boolean }[]> {
+ ): Promise {
const {
trackedEventTableName,
eventColumn,
@@ -1399,14 +1407,14 @@ export default abstract class SqlIntegration
} = this.getSchemaFormatConfig(schemaFormat);
const currentDateTime = new Date();
- const sevenDaysAgo = new Date(
- currentDateTime.valueOf() - 7 * 60 * 60 * 24 * 1000
+ //MKTODO: Re-think how to name this so if we change the calculation, it doesn't make the variable name incorrect
+ const ThirtyDaysAgo = new Date(
+ currentDateTime.valueOf() - 30 * 60 * 60 * 24 * 1000
);
const sql = `
SELECT
${eventColumn} as event,
- ${timestampColumn},
(CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId,
COUNT (*) as count,
MAX(${timestampColumn}) as lastTrackedAt
@@ -1414,9 +1422,10 @@ export default abstract class SqlIntegration
${this.generateTableName(trackedEventTableName)}
WHERE received_at < '${currentDateTime
.toISOString()
- .slice(0, 10)}' AND received_at > '${sevenDaysAgo
- .toISOString()
- .slice(0, 10)}'
+ .slice(
+ 0,
+ 10
+ )}' AND received_at > '${ThirtyDaysAgo.toISOString().slice(0, 10)}'
AND event NOT IN ('experiment_viewed', 'experiment_started')
GROUP BY event, ${timestampColumn}`;
@@ -1425,8 +1434,7 @@ export default abstract class SqlIntegration
if (schemaFormat === "segment") {
const pageViewedSql = `
SELECT
- 'page-viewed' as event,
- ${timestampColumn},
+ 'pages' as event,
(CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId,
COUNT (*) as count,
MAX(${timestampColumn}) as lastTrackedAt
@@ -1447,12 +1455,11 @@ export default abstract class SqlIntegration
throw new Error(`No events found.`);
}
- const formattedResults = this.formatTrackedEventResults(
- results,
- schemaFormat
- );
+ const formattedResults: Omit<
+ AutoGeneratedMetric,
+ "type" | "valueClause" | "groupByClause"
+ >[] = this.formatTrackedEventResults(results, schemaFormat);
- // Now that we have the formattedResults, is there where we should build logic to define additional metrics?
return this.generateMetricsToCreate(
formattedResults,
userIdColumn,
@@ -1478,17 +1485,7 @@ export default abstract class SqlIntegration
}
getAutoGeneratedMetricSqlQuery(
- metric: {
- event: string;
- displayName: string;
- hasUserId: boolean;
- createForUser: boolean;
- lastTrackedAt: Date;
- count: number;
- valueClause?: string;
- type?: MetricType;
- groupByClause?: string;
- },
+ metric: AutoGeneratedMetric,
schemaFormat: SchemaFormat
): string {
const {
diff --git a/packages/back-end/src/jobs/createAutomaticMetrics.ts b/packages/back-end/src/jobs/createAutomaticMetrics.ts
index d7239488ba6..6e03d72e62a 100644
--- a/packages/back-end/src/jobs/createAutomaticMetrics.ts
+++ b/packages/back-end/src/jobs/createAutomaticMetrics.ts
@@ -3,27 +3,18 @@ import Agenda, { Job } from "agenda";
import uniqid from "uniqid";
import { getDataSourceById } from "../models/DataSourceModel";
import { insertMetrics } from "../models/MetricModel";
-import { MetricInterface, MetricType } from "../../types/metric";
+import { MetricInterface } from "../../types/metric";
import { getSourceIntegrationObject } from "../services/datasource";
import { getInformationSchemaById } from "../models/InformationSchemaModel";
import { logger } from "../util/logger";
+import { AutoGeneratedMetric } from "../types/Integration";
const CREATE_AUTOMATIC_METRICS_JOB_NAME = "createAutomaticMetrics";
type CreateAutomaticMetricsJob = Job<{
organization: string;
datasourceId: string;
- metricsToCreate: {
- event: string;
- hasUserId: boolean;
- createForUser: boolean;
- displayName: string;
- lastTrackedAt: Date;
- count: number;
- type: MetricType;
- valueClause: string;
- groupByClause: string;
- }[];
+ metricsToCreate: AutoGeneratedMetric[];
}>;
function toSnakeCase(s: string): string {
@@ -65,23 +56,23 @@ export default function (ag: Agenda) {
if (!informationSchema) throw new Error("No information schema");
- let informationSchemaTableId = "";
+ // let informationSchemaTableId = "";
const metrics: Partial[] = [];
for (const metric of metricsToCreate) {
if (metric.createForUser) {
- informationSchema.databases.forEach((database) => {
- database.schemas.forEach((schema) => {
- schema.tables.forEach((table) => {
- if (
- table.tableName === metric.event ||
- table.tableName === toSnakeCase(metric.event)
- ) {
- informationSchemaTableId = table.id;
- }
- });
- });
- });
+ // informationSchema.databases.forEach((database) => {
+ // database.schemas.forEach((schema) => {
+ // schema.tables.forEach((table) => {
+ // if (
+ // table.tableName === metric.event ||
+ // table.tableName === toSnakeCase(metric.event)
+ // ) {
+ // informationSchemaTableId = table.id;
+ // }
+ // });
+ // });
+ // });
// const {
// tableData,
diff --git a/packages/back-end/src/types/Integration.ts b/packages/back-end/src/types/Integration.ts
index a3aac48d5ea..5b4c39e5921 100644
--- a/packages/back-end/src/types/Integration.ts
+++ b/packages/back-end/src/types/Integration.ts
@@ -131,6 +131,18 @@ export type PastExperimentResult = {
}[];
};
+export type AutoGeneratedMetric = {
+ event: string;
+ hasUserId: boolean;
+ createForUser: boolean;
+ displayName: string;
+ lastTrackedAt: Date | null;
+ count: number;
+ type?: MetricType;
+ valueClause?: string;
+ groupByClause?: string;
+};
+
export type MetricValueQueryResponseRow = {
date: string;
count: number;
@@ -295,13 +307,9 @@ export interface SourceIntegrationInterface {
runPastExperimentQuery(query: string): Promise;
getEventsTrackedByDatasource?: (
schemaFormat: SchemaFormat
- ) => Promise<{ event: string; hasUserId: boolean; createForUser: boolean }[]>;
+ ) => Promise;
getAutoGeneratedMetricSqlQuery?(
- metric: {
- event: string;
- hasUserId: boolean;
- createForUser: boolean;
- },
+ metric: AutoGeneratedMetric,
schemaFormat: SchemaFormat
): string;
}
From 5a161ec128f372fb7220e00bd8bfebbff8634762 Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Fri, 26 May 2023 10:18:51 -0400
Subject: [PATCH 17/39] Includes a big refactor that simplifies things a lot -
we're now getting all tracked events, and giving the user the option to
create a binomial and/or count metric for each event.
---
.../src/integrations/SqlIntegration.ts | 180 +++++-------------
.../src/jobs/createAutomaticMetrics.ts | 130 ++++---------
packages/back-end/src/types/Integration.ts | 9 +-
packages/back-end/types/datasource.d.ts | 1 +
.../components/Settings/NewDataSourceForm.tsx | 146 ++++++++++++--
5 files changed, 222 insertions(+), 244 deletions(-)
diff --git a/packages/back-end/src/integrations/SqlIntegration.ts b/packages/back-end/src/integrations/SqlIntegration.ts
index 6ea750dd4d3..453aaf7dc9c 100644
--- a/packages/back-end/src/integrations/SqlIntegration.ts
+++ b/packages/back-end/src/integrations/SqlIntegration.ts
@@ -1309,15 +1309,25 @@ export default abstract class SqlIntegration
getSchemaFormatConfig(schemaFormat: SchemaFormat): SchemaFormatConfig {
switch (schemaFormat) {
- // case "amplitude": TODO: Add support for amplitude later
- // return {
- // trackedEventTableName: "tracks",
- // eventColumn: "event",
- // timestampColumn: "server_upload_time",
- // userIdColumn: "user_id",
- // anonymousIdColumn: "amplitude_id",
- // };
- // Segment & Rudderstack
+ case "amplitude":
+ return {
+ trackedEventTableName: "tracks",
+ eventColumn: "event",
+ timestampColumn: "server_upload_time",
+ userIdColumn: "user_id",
+ anonymousIdColumn: "amplitude_id",
+ };
+ case "segment": {
+ return {
+ trackedEventTableName: "tracks",
+ eventColumn: "event",
+ timestampColumn: "received_at",
+ userIdColumn: "user_id",
+ anonymousIdColumn: "anonymous_id",
+ displayNameColumn: "event_text",
+ };
+ }
+ // Rudderstack
default:
return {
trackedEventTableName: "tracks",
@@ -1329,70 +1339,27 @@ export default abstract class SqlIntegration
}
}
- formatTrackedEventResults(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- results: any[],
- schemaFormat: SchemaFormat
- ): Omit[] {
- function toTitleCase(input: string): string {
- return input
- .replace(/^[-_]*(.)/, (_, c) => c.toUpperCase())
- .replace(/[-_]+(.)/g, (_, c) => " " + c.toUpperCase());
- }
-
- const formattedResults: Omit<
- AutoGeneratedMetric,
- "type" | "valueClause" | "groupByClause"
- >[] = [];
-
- results.forEach((result) => {
- result.event;
- result.createForUser = true;
- result.lastTrackedAt = new Date(result.lastTrackedAt.value);
- result.displayName = result.event;
- result.count;
-
- if (schemaFormat === "segment") {
- result.displayName =
- result.event === "pages" ? "Paged Viewed" : toTitleCase(result.event);
- }
- formattedResults.push(result);
- });
-
- return formattedResults;
- }
+ getAutoGeneratedMetricSqlQuery(
+ metric: AutoGeneratedMetric,
+ schemaFormat: SchemaFormat,
+ type: MetricType
+ ): string {
+ const {
+ timestampColumn,
+ userIdColumn,
+ anonymousIdColumn,
+ } = this.getSchemaFormatConfig(schemaFormat);
- generateMetricsToCreate(
- formattedResults: Omit<
- AutoGeneratedMetric,
- "type" | "valueClause" | "groupByClause"
- >[],
- timestampColumn: string,
- userIdColumn: string,
- anonymousIdColumn: string
- ): AutoGeneratedMetric[] {
- const metricsToCreate: AutoGeneratedMetric[] = [];
- formattedResults.forEach((result) => {
- const metric: AutoGeneratedMetric = {
- event: result.event,
- createForUser: true,
- lastTrackedAt: result.lastTrackedAt,
- hasUserId: !!result.hasUserId,
- count: result.count,
- displayName: result.displayName,
- };
- metricsToCreate.push(metric);
- if (result.event === "placed_order") {
- const newMetric = cloneDeep(metric);
- newMetric.displayName = "Average Order Value";
- newMetric.type = "revenue";
- newMetric.valueClause = "AVG(revenue)";
- newMetric.groupByClause = `GROUP BY ${userIdColumn}, ${anonymousIdColumn}, ${timestampColumn}`;
- metricsToCreate.push(newMetric);
- }
- });
+ const sqlQuery = `
+ SELECT
+ ${
+ metric.hasUserId ? `${userIdColumn}, ` : ""
+ }${anonymousIdColumn} as anonymous_id, ${timestampColumn} as timestamp
+ ${type === "count" && metric.createCountFromEvent ? `, 1 as value` : ""}
+ FROM ${this.generateTableName(metric.event)}
+ `;
- return metricsToCreate;
+ return format(sqlQuery, this.getFormatDialect());
}
async getEventsTrackedByDatasource(
@@ -1402,12 +1369,11 @@ export default abstract class SqlIntegration
trackedEventTableName,
eventColumn,
timestampColumn,
- userIdColumn,
- anonymousIdColumn,
+ displayNameColumn,
} = this.getSchemaFormatConfig(schemaFormat);
const currentDateTime = new Date();
- //MKTODO: Re-think how to name this so if we change the calculation, it doesn't make the variable name incorrect
+ //MKTODO: Re-think how to name & calculate this so if we change the calculation, it doesn't make the variable name incorrect
const ThirtyDaysAgo = new Date(
currentDateTime.valueOf() - 30 * 60 * 60 * 24 * 1000
);
@@ -1415,6 +1381,7 @@ export default abstract class SqlIntegration
const sql = `
SELECT
${eventColumn} as event,
+ ${displayNameColumn ? displayNameColumn : eventColumn} as displayName,
(CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId,
COUNT (*) as count,
MAX(${timestampColumn}) as lastTrackedAt
@@ -1427,7 +1394,7 @@ export default abstract class SqlIntegration
10
)}' AND received_at > '${ThirtyDaysAgo.toISOString().slice(0, 10)}'
AND event NOT IN ('experiment_viewed', 'experiment_started')
- GROUP BY event, ${timestampColumn}`;
+ GROUP BY event, ${timestampColumn}, displayName`;
const results = await this.runQuery(format(sql, this.getFormatDialect()));
@@ -1435,6 +1402,7 @@ export default abstract class SqlIntegration
const pageViewedSql = `
SELECT
'pages' as event,
+ "Page Viewed" as displayName,
(CASE WHEN COUNT(user_id) > 0 THEN 1 ELSE 0 END) as hasUserId,
COUNT (*) as count,
MAX(${timestampColumn}) as lastTrackedAt
@@ -1455,60 +1423,14 @@ export default abstract class SqlIntegration
throw new Error(`No events found.`);
}
- const formattedResults: Omit<
- AutoGeneratedMetric,
- "type" | "valueClause" | "groupByClause"
- >[] = this.formatTrackedEventResults(results, schemaFormat);
-
- return this.generateMetricsToCreate(
- formattedResults,
- userIdColumn,
- timestampColumn,
- anonymousIdColumn
- );
- }
-
- getAutoGeneratedMetricValueColumn(
- metricType: MetricType,
- schemaFormat: SchemaFormat
- ): string {
- const countColumn = schemaFormat === "rudderstack" ? "value" : "count";
-
- switch (metricType) {
- case "count":
- return `, ${countColumn} as value`;
- case "revenue":
- return ", revenue as value";
- default:
- return "";
- }
- }
-
- getAutoGeneratedMetricSqlQuery(
- metric: AutoGeneratedMetric,
- schemaFormat: SchemaFormat
- ): string {
- const {
- timestampColumn,
- userIdColumn,
- anonymousIdColumn,
- } = this.getSchemaFormatConfig(schemaFormat);
-
- const sqlQuery = `
- SELECT
- ${
- metric.hasUserId ? `${userIdColumn}, ` : ""
- }${anonymousIdColumn} as anonymous_id, ${timestampColumn} as timestamp
- ${
- metric.type !== "binomial" && metric.valueClause
- ? `, ${metric.valueClause} as value`
- : ""
- }
- FROM ${this.generateTableName(metric.event)}
- ${metric.groupByClause || ""}
- `;
-
- return format(sqlQuery, this.getFormatDialect());
+ return results.map((result) => {
+ result.createBinomialFromEvent = true;
+ result.createCountFromEvent = true;
+ result.lastTrackedAt = result.lastTrackedAt
+ ? new Date(result.lastTrackedAt)
+ : null;
+ return result;
+ });
}
private getMetricQueryFormat(metric: MetricInterface) {
diff --git a/packages/back-end/src/jobs/createAutomaticMetrics.ts b/packages/back-end/src/jobs/createAutomaticMetrics.ts
index 6e03d72e62a..6f29ac333bc 100644
--- a/packages/back-end/src/jobs/createAutomaticMetrics.ts
+++ b/packages/back-end/src/jobs/createAutomaticMetrics.ts
@@ -1,11 +1,11 @@
/* eslint-disable no-console */
import Agenda, { Job } from "agenda";
import uniqid from "uniqid";
+import { cloneDeep } from "lodash";
import { getDataSourceById } from "../models/DataSourceModel";
import { insertMetrics } from "../models/MetricModel";
import { MetricInterface } from "../../types/metric";
import { getSourceIntegrationObject } from "../services/datasource";
-import { getInformationSchemaById } from "../models/InformationSchemaModel";
import { logger } from "../util/logger";
import { AutoGeneratedMetric } from "../types/Integration";
@@ -17,10 +17,6 @@ type CreateAutomaticMetricsJob = Job<{
metricsToCreate: AutoGeneratedMetric[];
}>;
-function toSnakeCase(s: string): string {
- return s.replace(" ", "_").toLowerCase();
-}
-
let agenda: Agenda;
export default function (ag: Agenda) {
agenda = ag;
@@ -35,6 +31,13 @@ export default function (ag: Agenda) {
if (!datasource) throw new Error("No datasource");
+ const schemaFormat = datasource.settings.schemaFormat || "custom";
+
+ if (!schemaFormat || schemaFormat === "custom")
+ throw new Error(
+ `Unable to automatically generate metrics for a ${schemaFormat} schema format.`
+ );
+
const integration = getSourceIntegrationObject(datasource);
if (
@@ -45,90 +48,41 @@ export default function (ag: Agenda) {
"Auto generated metrics not supported for this data source"
);
- const informationSchemaId = datasource.settings.informationSchemaId;
-
- if (!informationSchemaId) throw new Error("No information schema id");
-
- const informationSchema = await getInformationSchemaById(
- organization,
- informationSchemaId
- );
-
- if (!informationSchema) throw new Error("No information schema");
-
- // let informationSchemaTableId = "";
-
const metrics: Partial[] = [];
for (const metric of metricsToCreate) {
- if (metric.createForUser) {
- // informationSchema.databases.forEach((database) => {
- // database.schemas.forEach((schema) => {
- // schema.tables.forEach((table) => {
- // if (
- // table.tableName === metric.event ||
- // table.tableName === toSnakeCase(metric.event)
- // ) {
- // informationSchemaTableId = table.id;
- // }
- // });
- // });
- // });
-
- // const {
- // tableData,
- // databaseName,
- // tableSchema,
- // tableName,
- // } = await fetchTableData(
- // datasource,
- // informationSchema,
- // informationSchemaTableId
- // );
-
- // if (!tableData) throw new Error("No table data");
-
- // const columns: Column[] = tableData?.map(
- // (row: { column_name: string; data_type: string }) => {
- // return {
- // columnName: row.column_name,
- // dataType: row.data_type,
- // path: getPath(datasource.type, {
- // tableCatalog: databaseName,
- // tableSchema: tableSchema,
- // tableName: tableName,
- // columnName: row.column_name,
- // }),
- // };
- // }
- // );
-
- const metricType = metric.type;
-
- // let metricType: MetricType = "binomial";
-
- // if (columns.length) {
- // if (columns.some((column) => column.columnName === "revenue")) {
- // metricType = "revenue";
- // } else if (
- // columns.some((column) => column.columnName === "count")
- // ) {
- // metricType = "count";
- // }
- // }
- const sqlQuery = integration.getAutoGeneratedMetricSqlQuery(
+ const baseMetric: Partial = {
+ organization,
+ datasource: datasourceId,
+ name: metric.displayName,
+ dateCreated: new Date(),
+ dateUpdated: new Date(),
+ };
+ if (metric.createBinomialFromEvent) {
+ const metricToCreate: Partial = cloneDeep(
+ baseMetric
+ );
+ metricToCreate.id = uniqid("met_");
+ metricToCreate.type = "binomial";
+ metricToCreate.sql = integration.getAutoGeneratedMetricSqlQuery(
+ metric,
+ schemaFormat,
+ "binomial"
+ );
+ metrics.push(metricToCreate);
+ }
+ if (metric.createCountFromEvent) {
+ const metricToCreate: Partial = cloneDeep(
+ baseMetric
+ );
+ metricToCreate.id = uniqid("met_");
+ metricToCreate.type = "count";
+ metricToCreate.name = `Number of ${metric.displayName}`; //TODO: This needs to be customized
+ metricToCreate.sql = integration.getAutoGeneratedMetricSqlQuery(
metric,
- integration.settings.schemaFormat || "custom"
+ schemaFormat,
+ "count"
);
- metrics.push({
- id: uniqid("met_"),
- organization,
- datasource: datasourceId,
- name: metric.displayName,
- type: metricType,
- sql: sqlQuery,
- dateCreated: new Date(),
- dateUpdated: new Date(),
- });
+ metrics.push(metricToCreate);
}
}
await insertMetrics(metrics);
@@ -145,11 +99,7 @@ export default function (ag: Agenda) {
export async function queueCreateAutomaticMetrics(
datasourceId: string,
organization: string,
- metricsToCreate: {
- event: string;
- hasUserId: boolean;
- createForUser: boolean;
- }[]
+ metricsToCreate: AutoGeneratedMetric[]
) {
if (!datasourceId || !organization || !metricsToCreate) return;
diff --git a/packages/back-end/src/types/Integration.ts b/packages/back-end/src/types/Integration.ts
index 5b4c39e5921..f56e5b086b1 100644
--- a/packages/back-end/src/types/Integration.ts
+++ b/packages/back-end/src/types/Integration.ts
@@ -134,13 +134,11 @@ export type PastExperimentResult = {
export type AutoGeneratedMetric = {
event: string;
hasUserId: boolean;
- createForUser: boolean;
+ createBinomialFromEvent: boolean;
+ createCountFromEvent: boolean;
displayName: string;
lastTrackedAt: Date | null;
count: number;
- type?: MetricType;
- valueClause?: string;
- groupByClause?: string;
};
export type MetricValueQueryResponseRow = {
@@ -310,6 +308,7 @@ export interface SourceIntegrationInterface {
) => Promise;
getAutoGeneratedMetricSqlQuery?(
metric: AutoGeneratedMetric,
- schemaFormat: SchemaFormat
+ schemaFormat: SchemaFormat,
+ type: MetricType
): string;
}
diff --git a/packages/back-end/types/datasource.d.ts b/packages/back-end/types/datasource.d.ts
index 54fbe41b95c..75861298fca 100644
--- a/packages/back-end/types/datasource.d.ts
+++ b/packages/back-end/types/datasource.d.ts
@@ -87,6 +87,7 @@ export interface SchemaFormatConfig {
trackedEventTableName: string;
eventColumn: string;
timestampColumn: string;
+ displayNameColumn?: string;
}
export interface DataSourceProperties {
diff --git a/packages/front-end/components/Settings/NewDataSourceForm.tsx b/packages/front-end/components/Settings/NewDataSourceForm.tsx
index 7eddfc8255e..4162ce2a776 100644
--- a/packages/front-end/components/Settings/NewDataSourceForm.tsx
+++ b/packages/front-end/components/Settings/NewDataSourceForm.tsx
@@ -69,7 +69,8 @@ const NewDataSourceForm: FC<{
{
event: string;
hasUserId: boolean;
- createForUser: boolean;
+ createBinomialFromEvent: boolean;
+ createCountFromEvent: boolean;
displayName: string;
lastTrackedAt: Date;
count: number;
@@ -113,7 +114,8 @@ const NewDataSourceForm: FC<{
metricsToCreate: {
event: string;
hasUserId: boolean;
- createForUser: boolean;
+ createBinomialFromEvent: boolean;
+ createCountFromEvent: boolean;
}[];
}>({
defaultValues: {
@@ -325,6 +327,16 @@ const NewDataSourceForm: FC<{
await updateSettings();
}
if (isFinalStep) {
+ if (metricsToCreate.length > 0) {
+ track("Generating Auto Metrics For User", {
+ metricsToCreate,
+ source,
+ type: datasource.type,
+ dataSourceId,
+ schema: schema,
+ newDatasourceForm: true,
+ });
+ }
// @ts-expect-error TS(2345) If you come across this, please fix it!: Argument of type 'string | null' is not assignable... Remove this comment to see the full error message
await onSuccess(newDataId);
onCancel && onCancel();
@@ -560,7 +572,7 @@ const NewDataSourceForm: FC<{
))}
{datasourceSupportsAutoGeneratedMetrics && (
- These are the metrics we've found that we can generate
- for you automatically. Once created, you can always edit
- these if you need to.
+ {`These are the tracked events we found from ${
+ schemasMap.get(schema)?.label || ""
+ } in your connected warehouse. We can use these events to automatically
+ generate the following metrics for you to give you a head
+ start on setting up your account.`}{" "}
+
+ You can always edit and remove these metrics at anytime
+ after they're created.
+
);
})}
From c99eccc746aaf1b9e7dbab301cfe1f7b073eb894 Mon Sep 17 00:00:00 2001
From: Michael Knowlton
Date: Fri, 26 May 2023 12:09:46 -0400
Subject: [PATCH 18/39] Improves the front end experience.
---
.../src/integrations/SqlIntegration.ts | 11 +-
packages/back-end/src/types/Integration.ts | 2 +-
.../components/Settings/NewDataSourceForm.tsx | 115 +++++++++++-------
3 files changed, 77 insertions(+), 51 deletions(-)
diff --git a/packages/back-end/src/integrations/SqlIntegration.ts b/packages/back-end/src/integrations/SqlIntegration.ts
index 453aaf7dc9c..ebe8bb6410d 100644
--- a/packages/back-end/src/integrations/SqlIntegration.ts
+++ b/packages/back-end/src/integrations/SqlIntegration.ts
@@ -1407,10 +1407,7 @@ export default abstract class SqlIntegration
COUNT (*) as count,
MAX(${timestampColumn}) as lastTrackedAt
FROM
- ${this.generateTableName("pages")}
- GROUP BY event, ${timestampColumn}
- ORDER BY lastTrackedAt DESC
- LIMIT 1`;
+ ${this.generateTableName("pages")}`;
const pageViewedResults = await this.runQuery(
format(pageViewedSql, this.getFormatDialect())
@@ -1426,9 +1423,9 @@ export default abstract class SqlIntegration
return results.map((result) => {
result.createBinomialFromEvent = true;
result.createCountFromEvent = true;
- result.lastTrackedAt = result.lastTrackedAt
- ? new Date(result.lastTrackedAt)
- : null;
+ result.lastTrackedAt = result.lastTrackedAt.value
+ ? new Date(result.lastTrackedAt.value)
+ : new Date(result.lastTrackedAt);
return result;
});
}
diff --git a/packages/back-end/src/types/Integration.ts b/packages/back-end/src/types/Integration.ts
index f56e5b086b1..5e07a4d615c 100644
--- a/packages/back-end/src/types/Integration.ts
+++ b/packages/back-end/src/types/Integration.ts
@@ -137,7 +137,7 @@ export type AutoGeneratedMetric = {
createBinomialFromEvent: boolean;
createCountFromEvent: boolean;
displayName: string;
- lastTrackedAt: Date | null;
+ lastTrackedAt: Date;
count: number;
};
diff --git a/packages/front-end/components/Settings/NewDataSourceForm.tsx b/packages/front-end/components/Settings/NewDataSourceForm.tsx
index 4162ce2a776..339ddad2b44 100644
--- a/packages/front-end/components/Settings/NewDataSourceForm.tsx
+++ b/packages/front-end/components/Settings/NewDataSourceForm.tsx
@@ -11,6 +11,7 @@ import {
} from "back-end/types/datasource";
import { useForm } from "react-hook-form";
import { cloneDeep } from "lodash";
+import { ago } from "@/../shared/dates";
import { useAuth } from "@/services/auth";
import track from "@/services/track";
import { getInitialSettings } from "@/services/datasources";
@@ -28,6 +29,8 @@ import Modal from "../Modal";
import { GBCircleArrowLeft } from "../Icons";
import Button from "../Button";
import Toggle from "../Forms/Toggle";
+import { DocLink } from "../DocLink";
+import Tooltip from "../Tooltip/Tooltip";
import EventSourceList from "./EventSourceList";
import ConnectionSettings from "./ConnectionSettings";
@@ -572,14 +575,18 @@ const NewDataSourceForm: FC<{
))}
{datasourceSupportsAutoGeneratedMetrics && (
-
Generate Metrics Automatically
+
+ Generate Metrics Automatically
+
+ New!
+
+
{metricsToCreate.length === 0 ? (
{`With ${
schemasMap.get(schema).label
- }, we may be able to generate metrics for you
- automatically,`}{" "}
+ }, we may be able to automatically generate metrics from your tracked events, `}
saving you and your team valuable time. (It's Free)
@@ -637,7 +644,7 @@ const NewDataSourceForm: FC<{
}}
color="outline-primary"
>
- Generate Metrics
+ See What Metrics We Can Create
@@ -647,42 +654,51 @@ const NewDataSourceForm: FC<{
{`These are the tracked events we found from ${
schemasMap.get(schema)?.label || ""
} in your connected warehouse. We can use these events to automatically
- generate the following metrics for you to give you a head
- start on setting up your account.`}{" "}
-
- You can always edit and remove these metrics at anytime
- after they're created.
-
+ generate the following metrics for you. And don't worry, you can always edit and remove these
+ metrics at anytime after they're created. `}
+
+ Click here to learn more about GrowthBook Metrics.
+
{metricsToCreate.length > 0 && (
-