diff --git a/.gitignore b/.gitignore index 212430b..e547a94 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ node/node_modules react/package-lock.json node/package-lock.json node/node_modules + + +.vscode/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4483015..211c6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ++ ### Added ++ - Quotation created metrics sent to Analytics Redshift ++ - Use quote metrics sent to Analytics Redshift + ## [1.5.5] - 2023-06-29 ### Fixed diff --git a/manifest.json b/manifest.json index 7f0c5ac..061e427 100644 --- a/manifest.json +++ b/manifest.json @@ -29,9 +29,7 @@ "vtex.my-account": "1.x", "vtex.my-account-commons": "1.x" }, - "registries": [ - "smartcheckout" - ], + "registries": ["smartcheckout"], "policies": [], "$schema": "https://raw.githubusercontent.com/vtex/node-vtex-api/master/gen/manifest.schema" } diff --git a/react/components/QuoteDetails/QuoteDetails.tsx b/react/components/QuoteDetails/QuoteDetails.tsx index 5babd39..6432083 100644 --- a/react/components/QuoteDetails/QuoteDetails.tsx +++ b/react/components/QuoteDetails/QuoteDetails.tsx @@ -38,6 +38,9 @@ import AlertMessage from './AlertMessage' import QuoteTable from './QuoteTable' import QuoteUpdateHistory from './QuoteUpdateHistory' import { Status } from '../../utils/status' +import { sendCreateQuoteMetric } from '../../utils/metrics/createQuote' +import type { UseQuoteMetricsParams } from '../../utils/metrics/useQuote' +import { sendUseQuoteMetric } from '../../utils/metrics/useQuote' const localStore = storageFactory(() => localStorage) const MAX_DISCOUNT_PERCENTAGE = 99 @@ -72,6 +75,8 @@ const QuoteDetails: FunctionComponent = () => { const { route: { params }, navigate, + workspace, + account, } = useRuntime() const isNewQuote = !params?.id @@ -196,6 +201,16 @@ const QuoteDetails: FunctionComponent = () => { }) .then((result: any) => { if (result.data.createQuote) { + const metricsParam = { + quoteId: result.data.createQuote, + sessionResponse, + workspace, + sendToSalesRep, + account, + } + + sendCreateQuoteMetric(metricsParam) + toastMessage(quoteMessages.createSuccess) handleClearCart(orderForm.orderFormId).then(() => { setQuoteState(initialState) @@ -292,6 +307,14 @@ const QuoteDetails: FunctionComponent = () => { setUsingQuoteState(false) }) .then(() => { + const metricsParam: UseQuoteMetricsParams = { + quoteState, + orderFormId: variables.orderFormId, + account, + sessionResponse, + } + + sendUseQuoteMetric(metricsParam) goToCheckout(checkoutUrl) setUsingQuoteState(false) }) diff --git a/react/package.json b/react/package.json index e8fd653..fe179b9 100644 --- a/react/package.json +++ b/react/package.json @@ -10,7 +10,8 @@ "graphql": "^14.5.8", "react": "^16.9.2", "react-apollo": "^3.1.5", - "react-intl": "^5.13.4" + "react-intl": "^5.13.4", + "axios": "1.4.0" }, "devDependencies": { "@types/jest": "^24.0.18", diff --git a/react/utils/metrics/createQuote.ts b/react/utils/metrics/createQuote.ts new file mode 100644 index 0000000..c5ee173 --- /dev/null +++ b/react/utils/metrics/createQuote.ts @@ -0,0 +1,135 @@ +import axios from 'axios' + +import type { Metric, SessionResponse } from './metrics' +import { sendMetric } from './metrics' + +const GRAPHQL_URL = (accountName: string, workspace?: string) => { + if (workspace) { + return `https://${workspace}--${accountName}.myvtex.com/_v/private/graphql/v1` + } + + return `https://${accountName}.myvtex.com/_v/private/graphql/v1` +} + +type CreateQuoteFieldsMetric = { + cost_center_id: string + cost_center_name: string + buyer_org_id: string + buyer_org_name: string + member_id: string + member_email: string + role: string + creation_date: string + quote_id: string + quote_reference_name: string + send_to_sales_rep: boolean +} + +type CreateQuoteMetricsParam = { + quoteId: string + sessionResponse: SessionResponse + workspace: string + account: string + sendToSalesRep: boolean +} + +type CreateQuoteMetric = Metric & { fields: CreateQuoteFieldsMetric } + +const fetchMetricsData = async ( + accountName: string, + workspace: string, + quoteId: string, + userEmail: string +) => { + const query = JSON.stringify({ + query: `query GetMetricsData($id: String, $email: String!) { + getQuote(id: $id) @context(provider: "vtex.b2b-quotes-graphql") { + organization + organizationName + costCenterName + referenceName + creatorRole + creationDate + }, + getUserByEmail(email: $email) @context(provider: "vtex.storefront-permissions") { + costId + } + }`, + variables: { id: quoteId, email: userEmail }, + }) + + const { data, errors } = ( + await axios.post(GRAPHQL_URL(accountName, workspace), query) + ).data + + if (errors) { + console.error('Graphql errors', errors) + throw new Error('Graphql Errors when trying get quote and user data') + } + + const quoteResult = data?.getQuote as Omit + const costId = (data?.getUserByEmail?.[0].costId ?? '') as string + + return { + ...quoteResult, + costId, + } +} + +const buildCreateQuoteMetric = async ( + metricsParam: CreateQuoteMetricsParam +): Promise => { + const { namespaces } = metricsParam.sessionResponse + const userEmail = namespaces?.profile?.email?.value + + const metricsData = await fetchMetricsData( + metricsParam.account, + metricsParam.workspace, + metricsParam.quoteId, + userEmail + ) + + const metric: CreateQuoteMetric = { + name: 'b2b-suite-buyerorg-data', + kind: 'create-quote-ui-event', + description: 'Create Quotation Action - UI', + account: metricsParam.account, + fields: { + buyer_org_id: metricsData.organization, + buyer_org_name: metricsData.organizationName, + cost_center_id: metricsData.costId, + cost_center_name: metricsData.costCenterName, + member_id: namespaces?.profile?.id?.value, + member_email: userEmail, + role: metricsData.creatorRole, + creation_date: metricsData.creationDate, + quote_id: metricsParam.quoteId, + quote_reference_name: metricsData.referenceName, + send_to_sales_rep: metricsParam.sendToSalesRep, + }, + } + + return metric +} + +type QuoteMetricsData = { + organization: string // organizationId + organizationName: string + costId: string + costCenterName: string + referenceName: string // quote reference name + creatorRole: string + creationDate: string +} + +export const sendCreateQuoteMetric = async ( + metricsParam: CreateQuoteMetricsParam +) => { + try { + const metric = await buildCreateQuoteMetric(metricsParam) + + await sendMetric(metric) + } catch (error) { + console.warn('Unable to log metrics', error) + } +} diff --git a/react/utils/metrics/metrics.ts b/react/utils/metrics/metrics.ts new file mode 100644 index 0000000..8bc530b --- /dev/null +++ b/react/utils/metrics/metrics.ts @@ -0,0 +1,37 @@ +import axios from 'axios' + +const ANALYTICS_URL = 'https://rc.vtex.com/api/analytics/schemaless-events' + +type CreateQuoteMetric = { + kind: 'create-quote-ui-event' + description: 'Create Quotation Action - UI' +} + +type UseQuoteMetric = { + kind: 'use-quote-ui-event' + description: 'Use Quotation Action - UI' +} + +export type Metric = { + name: 'b2b-suite-buyerorg-data' + account: string +} & (CreateQuoteMetric | UseQuoteMetric) + +export type SessionProfile = { + id: { value: string } + email: { value: string } +} + +export type SessionResponse = { + namespaces: { + profile: SessionProfile + } +} + +export const sendMetric = async (metric: Metric) => { + try { + await axios.post(ANALYTICS_URL, metric) + } catch (error) { + console.warn('Unable to log metrics', error) + } +} diff --git a/react/utils/metrics/useQuote.ts b/react/utils/metrics/useQuote.ts new file mode 100644 index 0000000..404f4db --- /dev/null +++ b/react/utils/metrics/useQuote.ts @@ -0,0 +1,65 @@ +import type { Metric, SessionResponse } from './metrics' +import { sendMetric } from './metrics' + +type UseQuoteFieldsMetric = { + quote_id: string + quote_reference_name: string + order_form_id: string + quote_creation_date: string + quote_use_date: string + creator_email: string + user_email: string + cost_center_name: string + buyer_org_id: string + buyer_org_name: string + quote_last_update: string +} + +type UseQuoteMetric = Metric & { fields: UseQuoteFieldsMetric } + +export type UseQuoteMetricsParams = { + quoteState: Quote + orderFormId: string + account: string + sessionResponse: SessionResponse +} + +const buildUseQuoteMetric = ( + metricsParam: UseQuoteMetricsParams +): UseQuoteMetric => { + const { quoteState, orderFormId, account, sessionResponse } = metricsParam + + const metric: UseQuoteMetric = { + name: 'b2b-suite-buyerorg-data', + kind: 'use-quote-ui-event', + description: 'Use Quotation Action - UI', + account, + fields: { + buyer_org_id: quoteState.organization, + buyer_org_name: quoteState.organizationName, + cost_center_name: quoteState.costCenterName, + quote_id: quoteState.id, + quote_reference_name: quoteState.referenceName, + order_form_id: orderFormId, + quote_creation_date: quoteState.creationDate, + quote_use_date: new Date().toISOString(), + creator_email: quoteState.creatorEmail, + user_email: sessionResponse.namespaces?.profile?.email?.value, + quote_last_update: quoteState.lastUpdate, + }, + } + + return metric +} + +export const sendUseQuoteMetric = async ( + metricsParam: UseQuoteMetricsParams +) => { + try { + const metric = buildUseQuoteMetric(metricsParam) + + await sendMetric(metric) + } catch (error) { + console.warn('Unable to log metrics', error) + } +} diff --git a/react/yarn.lock b/react/yarn.lock index 64c3cae..3d88805 100644 --- a/react/yarn.lock +++ b/react/yarn.lock @@ -2485,6 +2485,15 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +axios@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^25.5.1: version "25.5.1" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-25.5.1.tgz#bc2e6101f849d6f6aec09720ffc7bc5332e62853" @@ -2841,7 +2850,7 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -3330,6 +3339,11 @@ focus-visible@^5.2.0: resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3" integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ== +follow-redirects@^1.15.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -3340,6 +3354,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -4935,6 +4958,11 @@ prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + psl@^1.1.28: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"