feat: some mp analytics related to automate actions (#2299)

* fix(fe2): better resiliency for when mp cant be loaded

* WIP mixpanel track calls

* more resiliency improvements

* added all clientside tracking calls

* run finished event

* minor adjustment

* feat(automate): revert automationRunTriggerinAssociation

* feat(automate): track manual run triggers

* feat(automate): backend track automation run created events

* fix(automate): manual trigger type gql schema fix

* feat(automate): add source based filter to run trigger tracking

* fix(automate): fix trigger mock

* various minor adjustments

* remove comment

---------

Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com>
This commit is contained in:
Kristaps Fabians Geikins
2024-06-07 10:21:24 +03:00
committed by GitHub
parent 4c76d5f895
commit bdf27f6218
29 changed files with 318 additions and 82 deletions
@@ -109,6 +109,7 @@ import {
useAutomationInputEncryptor,
type AutomationInputEncryptor
} from '~/lib/automate/composables/automations'
import { useMixpanel } from '~/lib/core/composables/mp'
enum AutomationCreateSteps {
SelectFunction,
@@ -136,6 +137,8 @@ const props = defineProps<{
}>()
const open = defineModel<boolean>('open', { required: true })
const mixpanel = useMixpanel()
const { handleSubmit: handleDetailsSubmit } = useForm<DetailsFormValues>()
const stepsOrder = computed(() => [
@@ -443,6 +446,16 @@ const onDetailsSubmit = handleDetailsSubmit(async () => {
return
}
mixpanel.track('Automation Created', {
automationId: aId,
name,
projectId: project.id,
functionName: fn.name,
functionId: fn.id,
functionReleaseId: fnRelease.id,
modelId: model.id
})
// Enable
await updateAutomation(
{
@@ -57,6 +57,7 @@ import { useCreateAutomateFunction } from '~/lib/automate/composables/management
import { useMutationLoading } from '@vue/apollo-composable'
import type { AutomateFunctionCreateDialogDoneStep_AutomateFunctionFragment } from '~~/lib/common/generated/gql/graphql'
import { automationFunctionRoute } from '~/lib/common/helpers/route'
import { useMixpanel } from '~/lib/core/composables/mp'
enum FunctionCreateSteps {
Authorize,
@@ -74,6 +75,7 @@ const props = defineProps<{
}>()
const open = defineModel<boolean>('open', { required: true })
const mixpanel = useMixpanel()
const logger = useLogger()
const mutationLoading = useMutationLoading()
const createFunction = useCreateAutomateFunction()
@@ -96,6 +98,11 @@ const onDetailsSubmit = handleDetailsSubmit(async (values) => {
})
if (res?.id) {
mixpanel.track('Automate Function Created', {
functionId: res.id,
templateId: selectedTemplate.value.id,
name: values.name
})
createdFunction.value = res
step.value++
}
@@ -93,6 +93,7 @@ import {
useAutomationInputEncryptor,
type AutomationInputEncryptor
} from '~/lib/automate/composables/automations'
import { useMixpanel } from '~/lib/core/composables/mp'
type AutomationRevisionFunction =
ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunctionFragment
@@ -145,6 +146,7 @@ const createNewAutomationRevision = useCreateAutomationRevision()
const inputEncryption = useAutomationInputEncryptor({ ensureWhen: open })
const { triggerNotification } = useGlobalToast()
const logger = useLogger()
const mixpanel = useMixpanel()
const selectedModel = ref<CommonModelSelectorModelFragment>()
const selectedRelease = ref<SearchAutomateFunctionReleaseItemFragment>()
@@ -215,7 +217,7 @@ const onSave = async () => {
})
// TODO: Apollo cache mutation afterwards
await createNewAutomationRevision({
const res = await createNewAutomationRevision({
projectId: props.projectId,
input: {
automationId: props.automationId,
@@ -237,6 +239,15 @@ const onSave = async () => {
}
}
})
if (res?.id) {
mixpanel.track('Automation Revision Created', {
automationId: props.automationId,
projectId: props.projectId,
functionId: fId,
functionReleaseId: rId,
modelId: model.id
})
}
} finally {
automationEncrypt?.dispose()
loading.value = false
@@ -44,6 +44,7 @@ import type {
ProjectPageAutomationHeader_ProjectFragment
} from '~/lib/common/generated/gql/graphql'
import { projectRoute } from '~/lib/common/helpers/route'
import { useMixpanel } from '~/lib/core/composables/mp'
import { useUpdateAutomation } from '~/lib/projects/composables/automationManagement'
graphql(`
@@ -80,6 +81,7 @@ const props = defineProps<{
const switchId = useId()
const loading = useMutationLoading()
const updateAutomation = useUpdateAutomation()
const mixpanel = useMixpanel()
const automationsLink = computed(() => projectRoute(props.project.id, 'automations'))
const name = computed({
@@ -122,7 +124,7 @@ const enabled = computed({
enabled: newVal
}
}
await updateAutomation(args, {
const res = await updateAutomation(args, {
optimisticResponse: {
projectMutations: {
automationMutations: {
@@ -139,6 +141,14 @@ const enabled = computed({
failure: `Failed to ${args.input.enabled ? 'enable' : 'disable'} automation`
}
})
if (res?.id) {
mixpanel.track('Automation Enabled/Disabled', {
automationId: res.id,
automationName: res.name,
projectId: props.project.id,
enabled: res.enabled
})
}
}
})
</script>
@@ -24,6 +24,7 @@ import { ArrowPathIcon } from '@heroicons/vue/24/outline'
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
import { graphql } from '~/lib/common/generated/gql'
import type { ProjectPageAutomationRuns_AutomationFragment } from '~/lib/common/generated/gql/graphql'
import { useMixpanel } from '~/lib/core/composables/mp'
import { useTriggerAutomation } from '~/lib/projects/composables/automationManagement'
import { projectAutomationPagePaginatedRunsQuery } from '~/lib/projects/graphql/queries'
@@ -32,6 +33,7 @@ import { projectAutomationPagePaginatedRunsQuery } from '~/lib/projects/graphql/
graphql(`
fragment ProjectPageAutomationRuns_Automation on Automation {
id
name
enabled
isTestAutomation
runs(limit: 10) {
@@ -65,8 +67,18 @@ const { identifier, onInfiniteLoad } = usePaginatedQuery({
resolveCursorFromVariables: (vars) => vars.cursor
})
const triggerAutomation = useTriggerAutomation()
const mixpanel = useMixpanel()
const onTrigger = () => {
triggerAutomation(props.projectId, props.automation.id)
const onTrigger = async () => {
const res = await triggerAutomation(props.projectId, props.automation.id)
if (res) {
mixpanel.track('Automation Run Triggered', {
automationId: props.automation.id,
automationName: props.automation.name,
automationRunId: res,
projectId: props.projectId,
source: 'manual'
})
}
}
</script>
@@ -56,7 +56,7 @@ const documents = {
"\n fragment ProjectPageAutomationFunctions_Automation on Automation {\n id\n currentRevision {\n id\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevision\n functions {\n release {\n id\n function {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n releases(limit: 1) {\n items {\n id\n }\n }\n }\n }\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction\n }\n }\n }\n": types.ProjectPageAutomationFunctions_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationHeader_Automation on Automation {\n id\n name\n enabled\n isTestAutomation\n currentRevision {\n id\n triggerDefinitions {\n ... on VersionCreatedTriggerDefinition {\n model {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n }\n": types.ProjectPageAutomationHeader_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationHeader_Project on Project {\n id\n ...ProjectPageModelsCardProject\n }\n": types.ProjectPageAutomationHeader_ProjectFragmentDoc,
"\n fragment ProjectPageAutomationRuns_Automation on Automation {\n id\n enabled\n isTestAutomation\n runs(limit: 10) {\n items {\n ...AutomationRunDetails\n }\n totalCount\n cursor\n }\n }\n": types.ProjectPageAutomationRuns_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationRuns_Automation on Automation {\n id\n name\n enabled\n isTestAutomation\n runs(limit: 10) {\n items {\n ...AutomationRunDetails\n }\n totalCount\n cursor\n }\n }\n": types.ProjectPageAutomationRuns_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationsEmptyState_Query on Query {\n automateFunctions(limit: 9, filter: { featuredFunctionsOnly: true }) {\n items {\n ...AutomationsFunctionsCard_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n }\n }\n }\n": types.ProjectPageAutomationsEmptyState_QueryFragmentDoc,
"\n fragment ProjectPageAutomationsRow_Automation on Automation {\n id\n name\n enabled\n isTestAutomation\n currentRevision {\n id\n triggerDefinitions {\n ... on VersionCreatedTriggerDefinition {\n model {\n id\n name\n }\n }\n }\n }\n runs(limit: 10) {\n totalCount\n items {\n ...AutomationRunDetails\n }\n cursor\n }\n }\n": types.ProjectPageAutomationsRow_AutomationFragmentDoc,
"\n fragment ProjectDiscussionsPageHeader_Project on Project {\n id\n name\n }\n": types.ProjectDiscussionsPageHeader_ProjectFragmentDoc,
@@ -429,7 +429,7 @@ export function graphql(source: "\n fragment ProjectPageAutomationHeader_Projec
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageAutomationRuns_Automation on Automation {\n id\n enabled\n isTestAutomation\n runs(limit: 10) {\n items {\n ...AutomationRunDetails\n }\n totalCount\n cursor\n }\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationRuns_Automation on Automation {\n id\n enabled\n isTestAutomation\n runs(limit: 10) {\n items {\n ...AutomationRunDetails\n }\n totalCount\n cursor\n }\n }\n"];
export function graphql(source: "\n fragment ProjectPageAutomationRuns_Automation on Automation {\n id\n name\n enabled\n isTestAutomation\n runs(limit: 10) {\n items {\n ...AutomationRunDetails\n }\n totalCount\n cursor\n }\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationRuns_Automation on Automation {\n id\n name\n enabled\n isTestAutomation\n runs(limit: 10) {\n items {\n ...AutomationRunDetails\n }\n totalCount\n cursor\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -335,7 +335,6 @@ export enum AutomateRunStatus {
}
export enum AutomateRunTriggerType {
TestType = 'TEST_TYPE',
VersionCreated = 'VERSION_CREATED'
}
@@ -1831,7 +1830,7 @@ export type ProjectAutomationMutations = {
* Trigger an automation with a fake "version created" trigger. The "version created" will
* just refer to the last version of the model.
*/
trigger: Scalars['Boolean'];
trigger: Scalars['String'];
update: Automation;
};
@@ -3595,7 +3594,7 @@ export type ProjectPageAutomationHeader_AutomationFragment = { __typename?: 'Aut
export type ProjectPageAutomationHeader_ProjectFragment = { __typename?: 'Project', id: string, role?: string | null, visibility: ProjectVisibility };
export type ProjectPageAutomationRuns_AutomationFragment = { __typename?: 'Automation', id: string, enabled: boolean, isTestAutomation: boolean, runs: { __typename?: 'AutomateRunCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'AutomateRun', id: string, status: AutomateRunStatus, createdAt: string, updatedAt: string, functionRuns: Array<{ __typename?: 'AutomateFunctionRun', statusMessage?: string | null, id: string, status: AutomateRunStatus }>, trigger: { __typename?: 'VersionCreatedTrigger', version?: { __typename?: 'Version', id: string } | null, model?: { __typename?: 'Model', id: string } | null } }> } };
export type ProjectPageAutomationRuns_AutomationFragment = { __typename?: 'Automation', id: string, name: string, enabled: boolean, isTestAutomation: boolean, runs: { __typename?: 'AutomateRunCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'AutomateRun', id: string, status: AutomateRunStatus, createdAt: string, updatedAt: string, functionRuns: Array<{ __typename?: 'AutomateFunctionRun', statusMessage?: string | null, id: string, status: AutomateRunStatus }>, trigger: { __typename?: 'VersionCreatedTrigger', version?: { __typename?: 'Version', id: string } | null, model?: { __typename?: 'Model', id: string } | null } }> } };
export type ProjectPageAutomationsEmptyState_QueryFragment = { __typename?: 'Query', automateFunctions: { __typename?: 'AutomateFunctionCollection', items: Array<{ __typename?: 'AutomateFunction', id: string, name: string, isFeatured: boolean, description: string, logo?: string | null, repo: { __typename?: 'BasicGitRepositoryMetadata', id: string, url: string, owner: string, name: string }, releases: { __typename?: 'AutomateFunctionReleaseCollection', items: Array<{ __typename?: 'AutomateFunctionRelease', id: string, inputSchema?: {} | null }> } }> } };
@@ -4092,7 +4091,7 @@ export type TriggerAutomationMutationVariables = Exact<{
}>;
export type TriggerAutomationMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', automationMutations: { __typename?: 'ProjectAutomationMutations', trigger: boolean } } };
export type TriggerAutomationMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', automationMutations: { __typename?: 'ProjectAutomationMutations', trigger: string } } };
export type CreateTestAutomationMutationVariables = Exact<{
projectId: Scalars['ID'];
@@ -4718,7 +4717,7 @@ export const CommonModelSelectorModelFragmentDoc = {"kind":"Document","definitio
export const ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageAutomationFunctionSettingsDialog_AutomationRevision"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomationRevision"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"triggerDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VersionCreatedTriggerDefinition"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"CommonModelSelectorModel"}}]}}]}}]}}]}}]} as unknown as DocumentNode<ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFragment, unknown>;
export const ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunctionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomationRevisionFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"release"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"function"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunctionFragment, unknown>;
export const ProjectPageAutomationFunctions_AutomationFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageAutomationFunctions_Automation"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Automation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"currentRevision"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageAutomationFunctionSettingsDialog_AutomationRevision"}},{"kind":"Field","name":{"kind":"Name","value":"functions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"release"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"function"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomationsFunctionsCard_AutomateFunction"}},{"kind":"Field","name":{"kind":"Name","value":"releases"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction"}}]}}]}}]}}]} as unknown as DocumentNode<ProjectPageAutomationFunctions_AutomationFragment, unknown>;
export const ProjectPageAutomationRuns_AutomationFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageAutomationRuns_Automation"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Automation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"isTestAutomation"}},{"kind":"Field","name":{"kind":"Name","value":"runs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomationRunDetails"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}}]}}]} as unknown as DocumentNode<ProjectPageAutomationRuns_AutomationFragment, unknown>;
export const ProjectPageAutomationRuns_AutomationFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageAutomationRuns_Automation"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Automation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"isTestAutomation"}},{"kind":"Field","name":{"kind":"Name","value":"runs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomationRunDetails"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}}]}}]} as unknown as DocumentNode<ProjectPageAutomationRuns_AutomationFragment, unknown>;
export const ProjectPageAutomationPage_AutomationFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageAutomationPage_Automation"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Automation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageAutomationHeader_Automation"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageAutomationFunctions_Automation"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageAutomationRuns_Automation"}}]}}]} as unknown as DocumentNode<ProjectPageAutomationPage_AutomationFragment, unknown>;
export const ProjectPageAutomationHeader_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageAutomationHeader_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageModelsCardProject"}}]}}]} as unknown as DocumentNode<ProjectPageAutomationHeader_ProjectFragment, unknown>;
export const ProjectPageAutomationPage_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageAutomationPage_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageAutomationHeader_Project"}}]}}]} as unknown as DocumentNode<ProjectPageAutomationPage_ProjectFragment, unknown>;
@@ -1,4 +1,3 @@
import type { OverridedMixpanel } from 'mixpanel-browser'
import { useOnAuthStateChange } from '~/lib/auth/composables/auth'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import { md5 } from '~/lib/common/helpers/encodeDecode'
@@ -17,16 +16,10 @@ function getMixpanelServerId(): string {
/**
* Get Mixpanel instance
* Note: Mixpanel is not available during SSR because mixpanel-browser only works in the browser!
* If this composable is invoked during SSR it will return undefined!
*/
export function useMixpanel(): OverridedMixpanel {
// we're making TS lie here cause we don't want to constantly check if the return of this
// is undefined
if (process.server) return undefined as unknown as OverridedMixpanel
export function useMixpanel() {
const nuxt = useNuxtApp()
const $mixpanel = nuxt.$mixpanel as () => OverridedMixpanel
const $mixpanel = nuxt.$mixpanel
return $mixpanel()
}
@@ -193,7 +193,7 @@ export const useTriggerAutomation = () => {
})
}
return !!res?.data?.projectMutations?.automationMutations?.trigger
return res?.data?.projectMutations?.automationMutations?.trigger
}
}
+55 -9
View File
@@ -1,24 +1,67 @@
/* eslint-disable camelcase */
import { LogicError } from '@speckle/ui-components'
import type { OverridedMixpanel } from 'mixpanel-browser'
import type { Merge } from 'type-fest'
/**
* mixpanel-browser only supports being ran on the client-side (hence the name)! So it's only going to be accessible
* in client-side execution branches
*/
type LimitedMixpanel = Merge<
Pick<
OverridedMixpanel,
'track' | 'init' | 'reset' | 'register' | 'identify' | 'people' | 'add_group'
>,
{
people: Pick<OverridedMixpanel['people'], 'set' | 'set_once'>
}
>
const fakeLimitedMixpanel = (): LimitedMixpanel => ({
init: noop as LimitedMixpanel['init'],
track: noop,
reset: noop,
register: noop,
identify: noop,
people: {
set: noop,
set_once: noop
},
add_group: noop
})
export default defineNuxtPlugin(async () => {
const {
public: { mixpanelApiHost, mixpanelTokenId, logCsrEmitProps }
} = useRuntimeConfig()
const logger = useLogger()
const mixpanel = process.client
? (await import('mixpanel-browser')).default
: undefined
if (!mixpanel) {
return {
provide: {
mixpanel: () => {
throw new Error('Mixpanel is only available on the client-side!')
let mixpanel: LimitedMixpanel | undefined = undefined
try {
mixpanel = process.client ? (await import('mixpanel-browser')).default : undefined
if (process.server) {
mixpanel = {
...fakeLimitedMixpanel(),
track: () => {
throw new Error('mixpanel is not available on the server-side')
},
identify: () => {
throw new Error('mixpanel is not available on the server-side')
},
register: () => {
throw new Error('mixpanel is not available on the server-side')
}
}
}
} catch (e) {
logger.warn(e, 'Failed to load mixpanel')
}
if (!mixpanel) {
// Implement mocked version
mixpanel = fakeLimitedMixpanel()
}
// Init
@@ -30,7 +73,10 @@ export default defineNuxtPlugin(async () => {
return {
provide: {
mixpanel: () => mixpanel
mixpanel: () => {
if (!mixpanel) throw new LogicError('Mixpanel unexpectedly not defined')
return mixpanel
}
}
}
})
@@ -11,7 +11,6 @@ enum AutomateRunStatus {
enum AutomateRunTriggerType {
VERSION_CREATED
TEST_TYPE
}
type AutomationRevisionFunction {
@@ -296,7 +295,7 @@ type ProjectAutomationMutations {
Trigger an automation with a fake "version created" trigger. The "version created" will
just refer to the last version of the model.
"""
trigger(automationId: ID!): Boolean!
trigger(automationId: ID!): String!
createTestAutomation(input: ProjectTestAutomationCreateInput!): Automation!
createTestAutomationRun(automationId: ID!): TestAutomationRun!
}
@@ -1,8 +1,10 @@
import {
AutomationFunctionRunRecord,
AutomationRunRecord,
AutomationTriggerType,
AutomationWithRevision,
BaseTriggerManifest
BaseTriggerManifest,
RunTriggerSource
} from '@/modules/automate/helpers/types'
import { InsertableAutomationRun } from '@/modules/automate/repositories/automations'
import { initializeModuleEventEmitter } from '@/modules/shared/services/moduleEventEmitterSetup'
@@ -17,10 +19,12 @@ export type AutomateEventsPayloads = {
automation: AutomationWithRevision
run: InsertableAutomationRun
manifests: BaseTriggerManifest[]
source: RunTriggerSource
triggerType: AutomationTriggerType
}
[AutomateRunsEvents.StatusUpdated]: {
run: AutomationRunRecord
functionRuns: AutomationFunctionRunRecord[]
functionRun: AutomationFunctionRunRecord
automationId: string
}
}
@@ -506,14 +506,14 @@ export = (FF_AUTOMATE_MODULE_ENABLED
})
})
await trigger({
const { automationRunId } = await trigger({
automationId,
userId: ctx.userId!,
userResourceAccessRules: ctx.resourceAccessRules,
projectId: parent.projectId
})
return true
return automationRunId
},
async createTestAutomation(parent, { input }, ctx) {
const create = createTestAutomation({
@@ -596,6 +596,7 @@ export = (FF_AUTOMATE_MODULE_ENABLED
items: []
}
}
throw e
}
}
@@ -1,7 +1,4 @@
import {
TestTriggerType,
VersionCreationTriggerType
} from '@/modules/automate/helpers/types'
import { VersionCreationTriggerType } from '@/modules/automate/helpers/types'
import {
AutomateFunctionTemplateLanguage,
AutomateRunTriggerType
@@ -50,11 +47,9 @@ export const functionTemplateRepos = <const>[
]
export const dbToGraphqlTriggerTypeMap = <const>{
[VersionCreationTriggerType]: AutomateRunTriggerType.VersionCreated,
[TestTriggerType]: AutomateRunTriggerType.TestType
[VersionCreationTriggerType]: AutomateRunTriggerType.VersionCreated
}
export const graphqlToDbTriggerTypeMap = <const>{
[AutomateRunTriggerType.VersionCreated]: VersionCreationTriggerType,
[AutomateRunTriggerType.TestType]: TestTriggerType
[AutomateRunTriggerType.VersionCreated]: VersionCreationTriggerType
}
@@ -46,7 +46,7 @@ export type AutomationRunStatus =
| 'timeout'
| 'canceled'
export const AutomationRunStatuses: Record<AutomationRunStatus, AutomationRunStatus> = {
export const AutomationRunStatuses: { [key in AutomationRunStatus]: key } = {
pending: 'pending',
initializing: 'initializing',
running: 'running',
@@ -73,11 +73,13 @@ export type AutomateRevisionFunctionRecord = {
automationRevisionId: string
}
export enum RunTriggerSource {
Automatic = 'automatic',
Manual = 'manual'
}
export const VersionCreationTriggerType = <const>'versionCreation'
export const TestTriggerType = <const>'testtttt'
export type AutomationTriggerType =
| typeof VersionCreationTriggerType
| typeof TestTriggerType
export type AutomationTriggerType = typeof VersionCreationTriggerType
export type AutomationTriggerRecordBase<
T extends AutomationTriggerType = AutomationTriggerType
+9 -3
View File
@@ -8,9 +8,10 @@ import {
import { Environment } from '@speckle/shared'
import {
getActiveTriggerDefinitions,
getAutomationRunFullTriggers,
getFullAutomationRevisionMetadata,
getAutomation,
getAutomationRevision,
getAutomationRunFullTriggers
getAutomationRevision
} from '@/modules/automate/repositories/automations'
import { ScopeRecord } from '@/modules/auth/helpers/types'
import { Scopes } from '@speckle/shared'
@@ -26,6 +27,7 @@ import {
setupAutomationUpdateSubscriptions,
setupStatusUpdateSubscriptions
} from '@/modules/automate/services/subscriptions'
import { setupRunFinishedTracking } from '@/modules/automate/services/tracking'
import authGithubAppRest from '@/modules/automate/rest/authGithubApp'
const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags()
@@ -67,6 +69,9 @@ const initializeEventListeners = () => {
getAutomationRunFullTriggers
})
const setupAutomationUpdateSubscriptionsInvoke = setupAutomationUpdateSubscriptions()
const setupRunFinishedTrackingInvoke = setupRunFinishedTracking({
getFullAutomationRevisionMetadata
})
const quitters = [
VersionsEmitter.listen(
@@ -81,7 +86,8 @@ const initializeEventListeners = () => {
}
),
setupStatusUpdateSubscriptionsInvoke(),
setupAutomationUpdateSubscriptionsInvoke()
setupAutomationUpdateSubscriptionsInvoke(),
setupRunFinishedTrackingInvoke()
]
return () => {
@@ -35,6 +35,7 @@ import {
JsonSchemaInputValidationError
} from '@/modules/automate/errors/management'
import {
AutomationRunStatus,
AutomationRunStatuses,
VersionCreationTriggerType
} from '@/modules/automate/helpers/types'
@@ -561,7 +562,7 @@ export const getAutomationsStatus =
(a) => a.status === AutomationRunStatuses.pending
)
let status = AutomationRunStatuses.succeeded
let status: AutomationRunStatus = AutomationRunStatuses.succeeded
let statusMessage = 'All automations have succeeded'
if (failedAutomations.length) {
@@ -16,14 +16,14 @@ import {
import { Automate } from '@speckle/shared'
const AutomationRunStatusOrder: { [key in AutomationRunStatus]: number } = {
pending: 0,
initializing: 1,
running: 2,
succeeded: 3,
failed: 3,
canceled: 4,
exception: 5,
timeout: 5
[AutomationRunStatuses.pending]: 0,
[AutomationRunStatuses.initializing]: 1,
[AutomationRunStatuses.running]: 2,
[AutomationRunStatuses.succeeded]: 3,
[AutomationRunStatuses.failed]: 3,
[AutomationRunStatuses.exception]: 5,
[AutomationRunStatuses.timeout]: 5,
[AutomationRunStatuses.canceled]: 4
}
/**
@@ -148,7 +148,7 @@ export const reportFunctionRunStatus =
await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.StatusUpdated, {
run: updatedRun,
functionRuns: [nextFunctionRunRecord],
functionRun: nextFunctionRunRecord,
automationId
})
@@ -121,7 +121,7 @@ export const setupStatusUpdateSubscriptions =
AutomateRunsEmitter.listen(
AutomateRunsEmitter.events.StatusUpdated,
async ({ run, functionRuns, automationId }) => {
async ({ run, functionRun, automationId }) => {
const triggers = await getAutomationRunFullTriggers({
automationRunId: run.id
})
@@ -141,7 +141,7 @@ export const setupStatusUpdateSubscriptions =
versionId: trigger.version.id,
run: {
...run,
functionRuns,
functionRuns: [functionRun],
automationId,
triggers: undefined
},
@@ -0,0 +1,116 @@
import { automateLogger } from '@/logging/logging'
import { AutomateRunsEmitter } from '@/modules/automate/events/runs'
import {
AutomationFunctionRunRecord,
AutomationRunRecord,
AutomationRunStatus,
AutomationRunStatuses,
AutomationWithRevision,
RunTriggerSource
} from '@/modules/automate/helpers/types'
import {
InsertableAutomationRun,
getFullAutomationRevisionMetadata
} from '@/modules/automate/repositories/automations'
import { mixpanel } from '@/modules/shared/utils/mixpanel'
import { throwUncoveredError } from '@speckle/shared'
import dayjs from 'dayjs'
const isFinished = (runStatus: AutomationRunStatus) => {
const finishedStatuses: AutomationRunStatus[] = [
AutomationRunStatuses.succeeded,
AutomationRunStatuses.failed,
AutomationRunStatuses.exception,
AutomationRunStatuses.timeout,
AutomationRunStatuses.canceled
]
return finishedStatuses.includes(runStatus)
}
export type SetupRunFinishedTrackingDeps = {
getFullAutomationRevisionMetadata: typeof getFullAutomationRevisionMetadata
}
const onAutomationRunStatusUpdated =
({ getFullAutomationRevisionMetadata }: SetupRunFinishedTrackingDeps) =>
async ({
run,
functionRun,
automationId
}: {
run: AutomationRunRecord
functionRun: AutomationFunctionRunRecord
automationId: string
}) => {
if (!isFinished(run.status)) return
const automationWithRevision = await getFullAutomationRevisionMetadata(
run.automationRevisionId
)
if (!automationWithRevision) {
automateLogger.error(
{
run
},
'Run revision not found unexpectedly'
)
return
}
const mp = mixpanel({ userEmail: undefined })
await mp.track('Automate Function Run Finished', {
automationId,
automationRevisionId: automationWithRevision.id,
automationName: automationWithRevision.name,
runId: run.id,
functionRunId: functionRun.id,
status: functionRun.status,
durationInSeconds: dayjs(functionRun.updatedAt).diff(
functionRun.createdAt,
'second'
)
})
}
const onRunCreated = async ({
automation,
run: automationRun,
source
}: {
automation: AutomationWithRevision
run: InsertableAutomationRun
source: RunTriggerSource
}) => {
// all triggers, that are automatic result of an action are in a need to be tracked
switch (source) {
case RunTriggerSource.Automatic: {
const mp = mixpanel({ userEmail: undefined })
await mp.track('Automation Run Triggered', {
automationId: automation.id,
automationName: automation.name,
automationRunId: automationRun.id,
projectId: automation.projectId,
source
})
break
}
// runs created from a user interaction are tracked in the frontend
case RunTriggerSource.Manual:
return
default:
throwUncoveredError(source)
}
}
export const setupRunFinishedTracking = (deps: SetupRunFinishedTrackingDeps) => () => {
const quitters = [
AutomateRunsEmitter.listen(
AutomateRunsEmitter.events.StatusUpdated,
onAutomationRunStatusUpdated(deps)
),
AutomateRunsEmitter.listen(AutomateRunsEmitter.events.Created, onRunCreated)
]
return () => quitters.forEach((quitter) => quitter())
}
@@ -17,7 +17,8 @@ import {
VersionCreationTriggerType,
BaseTriggerManifest,
isVersionCreatedTriggerManifest,
LiveAutomation
LiveAutomation,
RunTriggerSource
} from '@/modules/automate/helpers/types'
import { getBranchLatestCommits } from '@/modules/core/repositories/branches'
import { getCommit } from '@/modules/core/repositories/commits'
@@ -219,9 +220,10 @@ export const triggerAutomationRevisionRun =
async <M extends BaseTriggerManifest = BaseTriggerManifest>(params: {
revisionId: string
manifest: M
source?: RunTriggerSource
}): Promise<{ automationRunId: string }> => {
const { automateRunTrigger } = deps
const { revisionId, manifest } = params
const { revisionId, manifest, source = RunTriggerSource.Automatic } = params
if (!isVersionCreatedTriggerManifest(manifest)) {
throw new AutomateInvalidTriggerError(
@@ -302,7 +304,9 @@ export const triggerAutomationRevisionRun =
await AutomateRunsEmitter.emit(AutomateRunsEmitter.events.Created, {
run: automationRun,
manifests: triggerManifests,
automation: automationWithRevision
automation: automationWithRevision,
source,
triggerType: manifest.triggerType
})
return { automationRunId: automationRun.id }
@@ -392,13 +396,18 @@ export const ensureRunConditions =
async function composeTriggerData(params: {
projectId: string
manifest: BaseTriggerManifest
// TODO: Q Gergo: What's going on here? Why do we pass in extra unrelated triggers?
triggerDefinitions: AutomationTriggerDefinitionRecord[]
}): Promise<BaseTriggerManifest[]> {
const { projectId, manifest, triggerDefinitions } = params
const manifests: BaseTriggerManifest[] = [{ ...manifest }]
/**
* The reason why we collect multiple triggers, even tho there's only one:
* - We want to collect the current context (all active versions of all triggers) at the time when the run is triggered,
* cause once the function already runs, there may be new versions already
*/
if (triggerDefinitions.length > 1) {
const associatedTriggers = triggerDefinitions.filter((t) => {
if (t.triggerType !== manifest.triggerType) return false
@@ -492,15 +501,17 @@ export const manuallyTriggerAutomation =
}
// Trigger "model version created"
return await triggerFunction({
const { automationRunId } = await triggerFunction({
revisionId: triggerDefs[0].automationRevisionId,
manifest: <VersionCreatedTriggerManifest>{
projectId,
modelId: latestCommit.branchId,
versionId: latestCommit.id,
triggerType: VersionCreationTriggerType
}
},
source: RunTriggerSource.Manual
})
return { automationRunId }
}
export type CreateTestAutomationRunDeps = {
@@ -17,6 +17,7 @@ import {
AutomationTriggerType,
BaseTriggerManifest,
LiveAutomation,
RunTriggerSource,
VersionCreatedTriggerManifest,
VersionCreationTriggerType,
isVersionCreatedTriggerManifest
@@ -294,7 +295,8 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags()
versionId: cryptoRandomString({ length: 10 }),
triggerType: VersionCreationTriggerType,
modelId: cryptoRandomString({ length: 10 })
}
},
source: RunTriggerSource.Manual
})
throw 'this should have thrown'
} catch (error) {
@@ -378,7 +380,8 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags()
versionId: version.id,
modelId: trigger.triggeringId,
triggerType: trigger.triggerType
}
},
source: RunTriggerSource.Manual
})
const storedRun = await getFullAutomationRunById(automationRunId)
@@ -468,7 +471,8 @@ const { FF_AUTOMATE_MODULE_ENABLED } = Environment.getFeatureFlags()
versionId: version.id,
modelId: trigger.triggeringId,
triggerType: trigger.triggerType
}
},
source: RunTriggerSource.Manual
})
const storedRun = await getFullAutomationRunById(automationRunId)
@@ -344,7 +344,6 @@ export enum AutomateRunStatus {
}
export enum AutomateRunTriggerType {
TestType = 'TEST_TYPE',
VersionCreated = 'VERSION_CREATED'
}
@@ -1845,7 +1844,7 @@ export type ProjectAutomationMutations = {
* Trigger an automation with a fake "version created" trigger. The "version created" will
* just refer to the last version of the model.
*/
trigger: Scalars['Boolean'];
trigger: Scalars['String'];
update: Automation;
};
@@ -4736,7 +4735,7 @@ export type ProjectAutomationMutationsResolvers<ContextType = GraphQLContext, Pa
createRevision?: Resolver<ResolversTypes['AutomationRevision'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsCreateRevisionArgs, 'input'>>;
createTestAutomation?: Resolver<ResolversTypes['Automation'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsCreateTestAutomationArgs, 'input'>>;
createTestAutomationRun?: Resolver<ResolversTypes['TestAutomationRun'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsCreateTestAutomationRunArgs, 'automationId'>>;
trigger?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsTriggerArgs, 'automationId'>>;
trigger?: Resolver<ResolversTypes['String'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsTriggerArgs, 'automationId'>>;
update?: Resolver<ResolversTypes['Automation'], ParentType, ContextType, RequireFields<ProjectAutomationMutationsUpdateArgs, 'input'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@@ -333,7 +333,6 @@ export enum AutomateRunStatus {
}
export enum AutomateRunTriggerType {
TestType = 'TEST_TYPE',
VersionCreated = 'VERSION_CREATED'
}
@@ -1834,7 +1833,7 @@ export type ProjectAutomationMutations = {
* Trigger an automation with a fake "version created" trigger. The "version created" will
* just refer to the last version of the model.
*/
trigger: Scalars['Boolean'];
trigger: Scalars['String'];
update: Automation;
};
+1 -1
View File
@@ -230,7 +230,7 @@ export async function buildMocksConfig(): Promise<{
...(isNullOrUndefined(enabled) ? {} : { enabled })
}
},
trigger: () => true,
trigger: () => faker.datatype.string(10),
createRevision: () => store.get('AutomationRevision') as any
},
UserAutomateInfo: {
@@ -334,7 +334,6 @@ export enum AutomateRunStatus {
}
export enum AutomateRunTriggerType {
TestType = 'TEST_TYPE',
VersionCreated = 'VERSION_CREATED'
}
@@ -1835,7 +1834,7 @@ export type ProjectAutomationMutations = {
* Trigger an automation with a fake "version created" trigger. The "version created" will
* just refer to the last version of the model.
*/
trigger: Scalars['Boolean'];
trigger: Scalars['String'];
update: Automation;
};
@@ -11,3 +11,12 @@ export function ensureError(
if (e instanceof Error) return e
return new UnexpectedErrorStructureError(fallbackMessage)
}
// this makes sure that a case is breaking in typing and in runtime too
export function throwUncoveredError(e: never): never {
throw createUncoveredError(e)
}
export function createUncoveredError(e: never) {
return new Error(`Uncovered error case ${e}.`)
}
+1 -1
View File
@@ -86,7 +86,7 @@
},
"files.eol": "\n",
"volar.vueserver.maxOldSpaceSize": 4000,
"cSpell.words": ["Automations", "Bursty", "mjml"],
"cSpell.words": ["Automations", "Bursty", "Insertable", "mjml"],
"tailwindCSS.experimental.configFile": {
"packages/frontend-2/tailwind.config.mjs": "packages/frontend-2/**"
},