Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creation Quote Metric #55

Merged
merged 25 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ node/node_modules
react/package-lock.json
node/package-lock.json
node/node_modules


.vscode/
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

+ ### Added
+ - Quotation created metrics sent to Analytics Redshift

## [1.5.5] - 2023-06-29

### Fixed
Expand Down
11 changes: 11 additions & 0 deletions react/components/QuoteDetails/QuoteDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import AlertMessage from './AlertMessage'
import QuoteTable from './QuoteTable'
import QuoteUpdateHistory from './QuoteUpdateHistory'
import { Status } from '../../utils/status'
import { sendMetric } from '../../utils/metrics'

const localStore = storageFactory(() => localStorage)
const MAX_DISCOUNT_PERCENTAGE = 99
Expand Down Expand Up @@ -72,6 +73,7 @@ const QuoteDetails: FunctionComponent = () => {
const {
route: { params },
navigate,
workspace,
mairatma marked this conversation as resolved.
Show resolved Hide resolved
} = useRuntime()

const isNewQuote = !params?.id
Expand Down Expand Up @@ -196,6 +198,15 @@ const QuoteDetails: FunctionComponent = () => {
})
.then((result: any) => {
if (result.data.createQuote) {
const metricsParam = {
quoteId: result.data.createQuote,
sessionResponse,
workspace,
sendToSalesRep,
}

sendMetric(metricsParam)

toastMessage(quoteMessages.createSuccess)
handleClearCart(orderForm.orderFormId).then(() => {
setQuoteState(initialState)
Expand Down
3 changes: 2 additions & 1 deletion react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
148 changes: 148 additions & 0 deletions react/utils/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import axios from 'axios'

const ANALYTICS_URL = 'https://rc.vtex.com/api/analytics/schemaless-events'

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 Metric = {
name: 'b2b-suite-buyerorg-data'
kind: 'create-quote-ui-event'
description: 'Create Quotation Action - UI'
account: string
}

type QuoteFieldsMetric = {
cost_center_id: string
cost_center_name: string
buy_org_id: string
buy_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
}

export type SessionProfile = {
id: { value: string }
email: { value: string }
}

type SessionResponse = {
namespaces: {
profile: SessionProfile
account: {
accountName: { value: string }
}
}
}

type MetricsParam = {
quoteId: string
sessionResponse: SessionResponse
workspace: string
sendToSalesRep: boolean
}

type QuoteMetric = Metric & { fields: QuoteFieldsMetric }

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
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a small indentation fix:

Suggested change
getUserByEmail(email: $email) @context(provider: "vtex.storefront-permissions") {
costId
}
getUserByEmail(email: $email) @context(provider: "vtex.storefront-permissions") {
costId
}

}`,
variables: { id: quoteId, email: userEmail },
})

const { data } = (
await axios.post(GRAPHQL_URL(accountName, workspace), query)
).data

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a GraphQL request fails, the response status is often 200 anyway, depending on the type of the error (mainly because there can be partial errors). So there would be no exception thrown here, but data might be undefined, or it might only have part of the information you requested. In those cases the response will contain a field called errors, so you can check for that here.

Suggested change
const { data } = (
await axios.post(GRAPHQL_URL(accountName, workspace), query)
).data
const { data, errors } = (
await axios.post(GRAPHQL_URL(accountName, workspace), query)
).data

More information on graphql errors here.

const quoteResult = data.getQuote as Omit<QuoteMetricsData, 'costId'>
const costId = data.getUserByEmail?.[0].costId as string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're using an optional operator here, so you're assuming that data.getUserByEmail might not be set, right? If that's a possibility then you shouldn't cast to string here, as it'll make costId's type not consider the use case where it will be undefined. Is it ok if costId isn't set? If it isn't it's better to bail early here, and if it's ok then you should let it be string | undefined.


return {
...quoteResult,
costId,
}
}

const buildQuoteMetric = async (
metricsParam: MetricsParam
): Promise<QuoteMetric> => {
const { namespaces } = metricsParam.sessionResponse
const accountName = namespaces.account.accountName.value
const userEmail = namespaces.profile.email.value

const metricsData = await fetchMetricsData(
accountName,
metricsParam.workspace,
metricsParam.quoteId,
userEmail
)

const metric: QuoteMetric = {
name: 'b2b-suite-buyerorg-data',
kind: 'create-quote-ui-event',
description: 'Create Quotation Action - UI',
account: accountName,
fields: {
buy_org_id: metricsData.organization,
buy_org_name: metricsData.organizationName,
cost_center_id: metricsData.costId,
cost_center_name: metricsData.costCenterName,
member_id: namespaces?.profile?.id?.value,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In line 98 the code is assuming that namespaces.profile.email.value is always set, but here you're handling the case where namespaces and profile might not be set. If that's a possibility we need to also add this to lines 97 and 98, skipping the whole metric process in case the values are not set (since those are required).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of skipping the while metric process, I'll check the namespace, profile, and email, and let it save null values. I believe it's better to save the metric with the data we have than not save it at all

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, if the metric is still useful, let's save it!

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 sendMetric = async (metricsParam: MetricsParam) => {
try {
const metric = await buildQuoteMetric(metricsParam)

await axios.post(ANALYTICS_URL, metric)
} catch (error) {
console.warn('Unable to log metrics', error)
}
}
30 changes: 29 additions & 1 deletion react/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==

[email protected]:
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"
Expand Down Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down