diff --git a/packages/client/package.json b/packages/client/package.json index 6efe2d976..cba07701e 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@braid/vue-formulate": "^2.5.3", + "@datadog/browser-rum": "^5.5.0", "bootstrap": "^4.6.1", "bootstrap-vue": "^2.19.0", "core-js": "^3.21.1", diff --git a/packages/client/src/arpa_reporter/main.js b/packages/client/src/arpa_reporter/main.js index 3be16ef44..60b9765a8 100644 --- a/packages/client/src/arpa_reporter/main.js +++ b/packages/client/src/arpa_reporter/main.js @@ -1,3 +1,10 @@ +/* eslint-disable import/first */ +import { datadogRum } from '@datadog/browser-rum'; + +if (window.APP_CONFIG?.DD_RUM_ENABLED === true) { + datadogRum.init(window.APP_CONFIG.DD_RUM_CONFIG); +} + import Vue from 'vue'; import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'; import App from './App.vue'; diff --git a/packages/client/src/components/Modals/GrantDetails.vue b/packages/client/src/components/Modals/GrantDetails.vue index b83ace7ee..6a9f4d7c9 100644 --- a/packages/client/src/components/Modals/GrantDetails.vue +++ b/packages/client/src/components/Modals/GrantDetails.vue @@ -22,6 +22,10 @@
{{ titleize(field) }}: {{ selectedGrant[field] }}
+ Category of Funding Activity: + {{ selectedGrant['funding_activity_categories']?.join(', ') }} +
Description:
The Division of Earth Sciences (EAR) awards Postdoctoral Fellowships
', eligibility_codes: '25', + funding_activity_category_codes: 'ST', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -68,6 +69,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Health Aide Program for Covid
', eligibility_codes: '11 07 25', + funding_activity_category_codes: 'HL ISS', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-06 16:03:53.57025-07', @@ -90,6 +92,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666999
', eligibility_codes: '25', + funding_activity_category_codes: 'HL', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -112,6 +115,7 @@ const grants = [ opportunity_category: 'Discretionary', description: '', eligibility_codes: '', + funding_activity_category_codes: 'HL', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-06 16:03:53.57025-07', @@ -134,6 +138,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Health Aide Program for Covid
', eligibility_codes: '11 07 25', + funding_activity_category_codes: 'O', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-06 16:03:53.57025-07', @@ -156,6 +161,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666666
', eligibility_codes: '25', + funding_activity_category_codes: 'ENV', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -178,6 +184,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666656
', eligibility_codes: '25', + funding_activity_category_codes: 'ST', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -200,6 +207,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666646
', eligibility_codes: '25', + funding_activity_category_codes: 'ISS', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -222,6 +230,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666636
', eligibility_codes: '25', + funding_activity_category_codes: 'O', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -244,6 +253,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666626
', eligibility_codes: '25', + funding_activity_category_codes: 'AR', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -266,6 +276,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666616
', eligibility_codes: '25', + funding_activity_category_codes: 'AG', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -288,6 +299,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666606
', eligibility_codes: '25', + funding_activity_category_codes: 'ISS', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -310,6 +322,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666596
', eligibility_codes: '25', + funding_activity_category_codes: '', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -354,6 +367,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666576
', eligibility_codes: '25', + funding_activity_category_codes: 'NR', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -376,6 +390,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666566
', eligibility_codes: '25', + funding_activity_category_codes: 'HL', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -398,6 +413,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666556
', eligibility_codes: '25', + funding_activity_category_codes: 'O', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -420,6 +436,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666546
', eligibility_codes: '25', + funding_activity_category_codes: 'NR', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -442,6 +459,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666536
', eligibility_codes: '25', + funding_activity_category_codes: 'ED ELT EN IS ST', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -486,6 +504,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666516
', eligibility_codes: '25', + funding_activity_category_codes: 'HL', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -508,6 +527,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666506
', eligibility_codes: '25', + funding_activity_category_codes: 'HL', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -530,6 +550,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666496
', eligibility_codes: '25', + funding_activity_category_codes: 'O', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -552,6 +573,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666486
', eligibility_codes: '25', + funding_activity_category_codes: 'ENV', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -574,6 +596,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666476
', eligibility_codes: '25', + funding_activity_category_codes: 'ISS', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -596,6 +619,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666466
', eligibility_codes: '25', + funding_activity_category_codes: 'ED', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -618,6 +642,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666456
', eligibility_codes: '25', + funding_activity_category_codes: 'ED ELT', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -640,6 +665,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666446
', eligibility_codes: '25', + funding_activity_category_codes: 'ST', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -662,6 +688,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666436
', eligibility_codes: '25', + funding_activity_category_codes: 'O', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -684,6 +711,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666426
', eligibility_codes: '25', + funding_activity_category_codes: 'O', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -706,6 +734,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666427 - zero ceil
', eligibility_codes: '25', + funding_activity_category_codes: 'O', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', @@ -728,6 +757,7 @@ const grants = [ opportunity_category: 'Discretionary', description: 'Test Grant Description 666428 - null ceil
', eligibility_codes: '25', + funding_activity_category_codes: 'HL', opportunity_status: 'posted', raw_body: 'raw body', created_at: '2021-08-11 11:30:38.89828-07', diff --git a/packages/server/src/configure.js b/packages/server/src/configure.js index 3f575e873..3d8b330ee 100755 --- a/packages/server/src/configure.js +++ b/packages/server/src/configure.js @@ -20,6 +20,7 @@ function configureApiRoutes(app) { app.use('/api/organizations/:organizationId/grants-saved-search', require('./routes/grantsSavedSearch')); app.use('/api/organizations/:organizationId/dashboard', require('./routes/dashboard')); app.use('/api/organizations/:organizationId/eligibility-codes', require('./routes/eligibilityCodes')); + app.use('/api/organizations/:organizationId/search-config', require('./routes/searchConfig')); app.use('/api/organizations/:organizationId/interested-codes', require('./routes/interestedCodes')); app.use('/api/organizations/:organizationId/keywords', require('./routes/keywords')); app.use('/api/organizations/:organizationId/refresh', require('./routes/refresh')); diff --git a/packages/server/src/db/index.js b/packages/server/src/db/index.js index c47a3948e..6d7b5c251 100755 --- a/packages/server/src/db/index.js +++ b/packages/server/src/db/index.js @@ -22,6 +22,7 @@ const moment = require('moment'); const knex = require('./connection'); const { TABLES } = require('./constants'); const emailConstants = require('../lib/email/constants'); +const { fundingActivityCategoriesByCode } = require('../lib/fieldConfigs/fundingActivityCategories'); const helpers = require('./helpers'); async function getUsers(tenantId) { @@ -495,6 +496,10 @@ function buildKeywordQuery(queryBuilder, includeKeywords, excludeKeywords, order return Boolean(includeExpression); } +function matchAsWordRegex(word) { + return `\\m${word}\\M`; +} + function buildFiltersQuery(queryBuilder, filters, agencyId) { const statusMap = { Applied: 'Result', @@ -538,6 +543,10 @@ function buildFiltersQuery(queryBuilder, filters, agencyId) { const date = moment().subtract(filters.postedWithinDays, 'days').startOf('day').format('YYYY-MM-DD'); qb.where(`${TABLES.grants}.open_date`, '>=', date); } + if (filters.fundingActivityCategories?.length) { + qb.where('funding_activity_category_codes', '~*', + filters.fundingActivityCategories.map(matchAsWordRegex).join('|')); + } }, ); } @@ -584,6 +593,7 @@ function grantsQuery(queryBuilder, filters, agencyId, orderingParams, pagination } } +// Convert saved search criteria to db query filters function formatSearchCriteriaToQueryFilters(criteria) { const parsedCriteria = JSON.parse(criteria); const postedWithinOptions = { @@ -614,6 +624,10 @@ function formatSearchCriteriaToQueryFilters(criteria) { filters.eligibilityCodes = parsedCriteria.eligibility.map((e) => e.code); delete parsedCriteria.eligibility; } + if (parsedCriteria.fundingActivityCategories) { + filters.fundingActivityCategories = parsedCriteria.fundingActivityCategories.map((c) => c.code); + delete parsedCriteria.fundingActivityCategories; + } filters = { ...filters, ...parsedCriteria }; return filters; @@ -623,6 +637,7 @@ function validateSearchFilters(filters) { const filterOptionsByType = { reviewStatuses: { type: 'List', valueType: 'Enum', values: ['Applied', 'Not Applying', 'Interested'] }, eligibilityCodes: { type: 'List', valueType: 'String' }, + fundingActivityCategories: { type: 'List', valueType: 'String' }, includeKeywords: { type: 'List', valueType: 'String' }, excludeKeywords: { type: 'List', valueType: 'String' }, opportunityNumber: { type: 'String', valueType: 'Any' }, @@ -769,6 +784,7 @@ async function getGrantsNew(filters, paginationParams, orderingParams, tenantId, 'grants.description_ts', 'grants.funding_instrument_codes', 'grants.bill', + 'grants.funding_activity_category_codes', ]) .select(knex.raw(` CASE @@ -811,6 +827,7 @@ async function getGrantsNew(filters, paginationParams, orderingParams, tenantId, 'grants.description_ts', 'grants.funding_instrument_codes', 'grants.bill', + 'grants.funding_activity_category_codes', ); if (toCsv) { query.modify(addCsvData); @@ -824,12 +841,14 @@ async function getGrantsNew(filters, paginationParams, orderingParams, tenantId, lastPage: Math.ceil(parseInt(fullCount, 10) / parseInt(paginationParams.perPage, 10)), }; - const dataWithAgency = await enhanceGrantData(tenantId, data); + const enhancedData = await enhanceGrantData(tenantId, data); - return { data: dataWithAgency, pagination }; + return { data: enhancedData, pagination }; } async function enhanceGrantData(tenantId, data) { + if (!data.length) return []; + const viewedByQuery = knex(TABLES.agencies) .join(TABLES.grants_viewed, `${TABLES.agencies}.id`, '=', `${TABLES.grants_viewed}.agency_id`) .whereIn('grant_id', data.map((grant) => grant.grant_id)) @@ -843,7 +862,7 @@ async function enhanceGrantData(tenantId, data) { ); const interestedBy = await getInterestedAgencies({ grantIds: data.map((grant) => grant.grant_id), tenantId }); - const dataWithAgency = data.map((grant) => { + const enhancedData = data.map((grant) => { const viewedByAgencies = viewedBy.filter((viewed) => viewed.grant_id === grant.grant_id); const agenciesInterested = interestedBy.filter((interested) => interested.grant_id === grant.grant_id); return { @@ -851,10 +870,14 @@ async function enhanceGrantData(tenantId, data) { etitle: decodeURIComponent(escape(grant.title)), viewed_by_agencies: viewedByAgencies, interested_agencies: agenciesInterested, + funding_activity_categories: (grant.funding_activity_category_codes || '') + .split(' ') + .map((code) => fundingActivityCategoriesByCode[code]?.name) + .filter(Boolean), }; }); - return dataWithAgency; + return enhancedData; } async function getGrants({ @@ -1025,23 +1048,8 @@ async function getSingleGrantDetails({ grantId, tenantId }) { const results = await knex.table(TABLES.grants) .select('*') .where({ grant_id: grantId }); - - const viewedBy = await knex(TABLES.agencies) - .join(TABLES.grants_viewed, `${TABLES.agencies}.id`, '=', `${TABLES.grants_viewed}.agency_id`) - .whereIn('grant_id', [grantId]) - .andWhere('tenant_id', tenantId) - .select(`${TABLES.grants_viewed}.grant_id`, `${TABLES.grants_viewed}.agency_id`, `${TABLES.agencies}.name as agency_name`, `${TABLES.agencies}.abbreviation as agency_abbreviation`); - - const interestedBy = await getInterestedAgencies({ grantIds: [grantId], tenantId }); - - const viewedByAgencies = viewedBy.filter((viewed) => viewed.grant_id === grantId); - const agenciesInterested = interestedBy.filter((interested) => interested.grant_id === grantId); - - return { - ...(results[0]), - viewed_by_agencies: viewedByAgencies, - interested_agencies: agenciesInterested, - }; + const enhancedResults = await enhanceGrantData(tenantId, results); + return enhancedResults.length ? enhancedResults[0] : null; } async function getClosestGrants({ diff --git a/packages/server/src/lib/fieldConfigs/fundingActivityCategories.js b/packages/server/src/lib/fieldConfigs/fundingActivityCategories.js new file mode 100644 index 000000000..3b49ee400 --- /dev/null +++ b/packages/server/src/lib/fieldConfigs/fundingActivityCategories.js @@ -0,0 +1,38 @@ +// Corresponds to "Category" facet on grants.gov. +const fundingActivityCategories = [ + { name: 'Affordable Care Act', code: 'ACA' }, + { name: 'Agriculture', code: 'AG' }, + { name: 'Arts', code: 'AR' }, + { name: 'Business and Commerce', code: 'BC' }, + { name: 'Community Development', code: 'CD' }, + { name: 'Consumer Protection', code: 'CP' }, + { name: 'Disaster Prevention and Relief', code: 'DPR' }, + { name: 'Education', code: 'ED' }, + { name: 'Employment, Labor and Training', code: 'ELT' }, + { name: 'Energy', code: 'EN' }, + { name: 'Environment', code: 'ENV' }, + { name: 'Food and Nutrition', code: 'FN' }, + { name: 'Health', code: 'HL' }, + { name: 'Housing', code: 'HO' }, + { name: 'Humanities', code: 'HU' }, + { name: 'Income Security and Social Services', code: 'ISS' }, + { name: 'Information and Statistics', code: 'IS' }, + { name: 'Infrastructure Investment and Jobs Act', code: 'IIJ' }, + { name: 'Law, Justice and Legal Services', code: 'LJL' }, + { name: 'Natural Resources', code: 'NR' }, + { name: 'Opportunity Zone Benefits', code: 'OZ' }, + { name: 'Other', code: 'O' }, + { name: 'Recovery Act', code: 'RA' }, + { name: 'Regional Development', code: 'RD' }, + { + name: 'Science and Technology and Other Research and Development', + code: 'ST', + }, + { name: 'Transportation', code: 'T' }, +]; + +const fundingActivityCategoriesByCode = fundingActivityCategories.reduce( + (obj, item) => Object.assign(obj, { [item.code]: item }), {}, +); + +module.exports = { fundingActivityCategories, fundingActivityCategoriesByCode }; diff --git a/packages/server/src/lib/grants-ingest.js b/packages/server/src/lib/grants-ingest.js index 14ed7cabf..db326641b 100644 --- a/packages/server/src/lib/grants-ingest.js +++ b/packages/server/src/lib/grants-ingest.js @@ -39,6 +39,7 @@ function mapSourceDataToGrant(source) { opportunity_category: source.opportunity.category.name, cfda_list: (source.cfda_numbers || []).join(', '), eligibility_codes: (source.eligible_applicants || []).map((it) => it.code).join(' '), + funding_activity_category_codes: (source.funding_activity?.categories || []).map((it) => it.code).join(' '), award_ceiling: source.award && source.award.ceiling ? source.award.ceiling : undefined, award_floor: source.award && source.award.floor ? source.award.floor : undefined, raw_body: JSON.stringify(source), diff --git a/packages/server/src/routes/grants.js b/packages/server/src/routes/grants.js index 8d4a298a6..3dc958cc8 100755 --- a/packages/server/src/routes/grants.js +++ b/packages/server/src/routes/grants.js @@ -51,6 +51,7 @@ function criteriaToFiltersObj(criteria, agencyId) { return { reviewStatuses: filters.reviewStatus?.split(',').filter((r) => r !== 'Assigned').map((r) => r.trim()) || [], eligibilityCodes: filters.eligibility?.split(',') || [], + fundingActivityCategories: filters.fundingActivityCategories?.split(',') || [], includeKeywords: filters.includeKeywords?.split(',').map((k) => k.trim()) || [], excludeKeywords: filters.excludeKeywords?.split(',').map((k) => k.trim()) || [], opportunityNumber: filters.opportunityNumber || '', @@ -128,6 +129,7 @@ router.get('/exportCSVNew', requireUser, async (req, res) => { // Generate CSV const formattedData = data.map((grant) => ({ ...grant, + funding_activity_categories: grant.funding_activity_categories.join('|'), interested_agencies: grant.interested_agencies .map((v) => v.agency_abbreviation) .join(', '), @@ -170,6 +172,7 @@ router.get('/exportCSVNew', requireUser, async (req, res) => { { key: 'bill', header: 'Appropriations Bill' }, { key: 'agency_code', header: 'Agency Code' }, { key: 'eligibility', header: 'Eligibility' }, + { key: 'funding_activity_categories', header: 'Category of Funding Activity' }, ], }); diff --git a/packages/server/src/routes/searchConfig.js b/packages/server/src/routes/searchConfig.js new file mode 100644 index 000000000..127135228 --- /dev/null +++ b/packages/server/src/routes/searchConfig.js @@ -0,0 +1,17 @@ +const express = require('express'); + +const router = express.Router({ mergeParams: true }); +const { fundingActivityCategories } = require('../lib/fieldConfigs/fundingActivityCategories'); +const db = require('../db'); +const { requireUser } = require('../lib/access-helpers'); + +router.get('/', requireUser, async (req, res) => { + const eligibilityCodes = await db.getAgencyEligibilityCodes(req.session.selectedAgency); + eligibilityCodes.forEach((ec) => { + delete ec.created_at; + delete ec.updated_at; + }); + res.json({ eligibilityCodes, fundingActivityCategories }); +}); + +module.exports = router; diff --git a/service.datadog.yaml b/service.datadog.yaml new file mode 100644 index 000000000..395335af7 --- /dev/null +++ b/service.datadog.yaml @@ -0,0 +1,35 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/DataDog/schema/main/service-catalog/v2.1/schema.json +schema-version: v2.1 +application: GOST +description: > + GOST is a platform that hosts tools to enable state and local government officials to + more easily apply for and report on their federal grants. +dd-service: gost +team: usdr-grants +contacts: + - type: slack + contact: https://usdigitalresponse.slack.com/archives/C0324KDQSCR +links: + - name: Source + type: repo + provider: github + url: https://github.com/usdigitalresponse/usdr-gost + - name: Service Monitoring Dashboard + type: dashboard + url: https://app.datadoghq.com/dashboard/kdw-rtz-5pb/ + - name: ARPA Audit Report Dashboard + type: dashboard + url: https://app.datadoghq.com/dashboard/5gk-g5h-p7t + - name: Project Development Board + type: other + url: https://github.com/orgs/usdigitalresponse/projects/5/views/1 + - name: Grants Program Wiki + type: doc + url: https://www.notion.so/usdr/Grants-Program-Wiki-44d6252c85344eb3b0077a9eb4f0fc5c + - name: Federal Grant Finder product documentation + type: doc + url: https://www.notion.so/usdr/Federal-Grant-Finder-54ffac3935ec478aa3c78c31e98a81ed + - name: ARPA Quarterly Reporter product documentation + type: doc + url: https://www.notion.so/usdr/ARPA-Quarterly-P-E-Reporter-ebd0a1ac2a7f4653b35517819befa9ea diff --git a/terraform/main.tf b/terraform/main.tf index e829a0143..49e76e07d 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -69,6 +69,18 @@ module "website" { origin_artifacts_dist_path = coalesce( var.website_origin_artifacts_dist_path, "${path.root}/../packages/client/dist" ) + + datadog_rum_enabled = var.website_datadog_rum_enabled + datadog_rum_config = merge(var.website_datadog_rum_options, { + applicationId = "15db471e-2ccb-4d3c-a6bf-99b750d748f5" + clientToken = "pub50834fcc1999d53e546519b1a0f03934" + site = "datadoghq.com" + service = local.unified_service_tags.service + env = local.unified_service_tags.env + version = local.unified_service_tags.version + defaultPrivacyLevel = "mask" + allowedTracingUrls = ["https://${local.api_domain_name}"] + }) } module "api_to_postgres_security_group" { diff --git a/terraform/modules/gost_api/gateway.tf b/terraform/modules/gost_api/gateway.tf index f61cc4e0f..aa1d04123 100644 --- a/terraform/modules/gost_api/gateway.tf +++ b/terraform/modules/gost_api/gateway.tf @@ -54,10 +54,15 @@ module "api_gateway" { allow_headers = [ "authorization", "content-type", + "traceparent", "x-amz-date", "x-amz-security-token", "x-amz-user-agent", "x-api-key", + "x-datadog-trace-id", + "x-datadog-parent-id", + "x-datadog-origin", + "x-datadog-sampling-priority", ] max_age = 86400 // 24 hours, in seconds } diff --git a/terraform/modules/gost_website/runtime_config.tf b/terraform/modules/gost_website/runtime_config.tf index 948596a62..c18c2c734 100644 --- a/terraform/modules/gost_website/runtime_config.tf +++ b/terraform/modules/gost_website/runtime_config.tf @@ -2,8 +2,10 @@ locals { deployConfigContents = templatefile( "${path.module}/tpl/deploy-config.js", { - gost_api_domain = var.gost_api_domain, - feature_flags = jsonencode(var.feature_flags), + gost_api_domain = var.gost_api_domain + feature_flags = jsonencode(var.feature_flags) + dd_rum_enabled = var.datadog_rum_enabled + dd_rum_config = jsonencode(var.datadog_rum_config) } ) } diff --git a/terraform/modules/gost_website/tpl/deploy-config.js b/terraform/modules/gost_website/tpl/deploy-config.js index 245fa6642..69ed4fa93 100644 --- a/terraform/modules/gost_website/tpl/deploy-config.js +++ b/terraform/modules/gost_website/tpl/deploy-config.js @@ -1,6 +1,10 @@ window.APP_CONFIG = window.APP_CONFIG || {}; window.APP_CONFIG.apiURLForGOST = 'https://${gost_api_domain}/'; window.apiURLForGOST = window.APP_CONFIG.apiURLForGOST; // Legacy + +window.APP_CONFIG.DD_RUM_ENABLED = ${dd_rum_enabled}; +window.APP_CONFIG.DD_RUM_CONFIG = ${dd_rum_config}; + window.APP_CONFIG.featureFlags = ${feature_flags}; window.APP_CONFIG.overrideFeatureFlag = (flagName, overrideValue) => { diff --git a/terraform/modules/gost_website/variables.tf b/terraform/modules/gost_website/variables.tf index 336661527..ac0452d2b 100644 --- a/terraform/modules/gost_website/variables.tf +++ b/terraform/modules/gost_website/variables.tf @@ -35,6 +35,37 @@ variable "gost_api_domain" { type = string } +variable "datadog_rum_enabled" { + description = "Whether to enable Datadog RUM." + type = bool + default = false +} + +variable "datadog_rum_config" { + description = "Runtime configuration options for Datadog RUM." + type = object({ + applicationId = string + clientToken = string + site = string + service = string + env = string + version = string + sessionSampleRate = number + sessionReplaySampleRate = number + trackUserInteractions = bool + trackResources = bool + trackLongTasks = bool + defaultPrivacyLevel = string + allowedTracingUrls = list(string) + }) + default = null + + validation { + condition = can(jsonencode(var.datadog_rum_config)) + error_message = "Value must be JSON-serializable." + } +} + variable "feature_flags" { description = "Feature flags for configuring the website runtime" type = any diff --git a/terraform/prod.tfvars b/terraform/prod.tfvars index 4d7814794..03d00dd8f 100644 --- a/terraform/prod.tfvars +++ b/terraform/prod.tfvars @@ -38,6 +38,14 @@ website_managed_waf_rules = { metric_visibility = true } } +website_datadog_rum_enabled = true +website_datadog_rum_options = { + sessionSampleRate = 80 + sessionReplaySampleRate = 20 + trackUserInteractions = true + trackResources = true + trackLongTasks = true +} website_feature_flags = { myProfileEnabled = false, newTerminologyEnabled = false diff --git a/terraform/staging.tfvars b/terraform/staging.tfvars index f40cf893e..116d6fdf7 100644 --- a/terraform/staging.tfvars +++ b/terraform/staging.tfvars @@ -35,6 +35,14 @@ website_managed_waf_rules = { metric_visibility = true } } +website_datadog_rum_enabled = true +website_datadog_rum_options = { + sessionSampleRate = 10 + sessionReplaySampleRate = 1 + trackUserInteractions = true + trackResources = true + trackLongTasks = true +} website_feature_flags = { myProfileEnabled = true, newTerminologyEnabled = true diff --git a/terraform/variables.tf b/terraform/variables.tf index 2982291dc..b3dc7feee 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -118,6 +118,30 @@ variable "website_origin_artifacts_dist_path" { default = "" } +variable "website_datadog_rum_enabled" { + description = "Whether to enable Datadog RUM on the website." + type = bool + default = false +} + +variable "website_datadog_rum_options" { + description = "Configuration options for Datadog RUM data collection (if var.website_datadog_rum_enabled is true)." + type = object({ + sessionSampleRate = number + sessionReplaySampleRate = number + trackUserInteractions = bool + trackResources = bool + trackLongTasks = bool + }) + default = { + sessionSampleRate = 100 + sessionReplaySampleRate = 20 + trackUserInteractions = true + trackResources = true + trackLongTasks = true + } +} + variable "website_feature_flags" { description = "Map of website feature flag names and their values." type = any diff --git a/yarn.lock b/yarn.lock index 4c224bb65..619ea26d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -221,14 +221,14 @@ "@aws-sdk/util-utf8-browser" "^3.0.0" tslib "^1.11.1" -"@aws-sdk/client-cognito-identity@3.470.0": - version "3.470.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.470.0.tgz#cb91e66c5e2712e89f05b4d8fcaefead5ee8096f" - integrity sha512-oE665xfl/KwvbcNtvUxMCKwh+X3wOV5UgPrPSptK+DzUJbtL4FAP7h6QIVzUB5CkzqhQVRAmYvdf+XhfXz3T3g== +"@aws-sdk/client-cognito-identity@3.473.0": + version "3.473.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.473.0.tgz#59981ad73897ffc427632a1f31c28fad16810ebf" + integrity sha512-/wYxFvlYxZrpHeN5XRJ4c8dk3ONuVX8ELAGQ7hnTYnqC3HhsUmL7egu4bhFu+N9QWsWO5BJtOfAoh2J7+IRrAw== dependencies: "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/client-sts" "3.470.0" + "@aws-sdk/client-sts" "3.473.0" "@aws-sdk/core" "3.468.0" "@aws-sdk/credential-provider-node" "3.470.0" "@aws-sdk/middleware-host-header" "3.468.0" @@ -267,14 +267,14 @@ tslib "^2.5.0" "@aws-sdk/client-s3@^3.312.0": - version "3.472.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.472.0.tgz#a4f5e3f2e7daaa345068ae205d95785d8343fead" - integrity sha512-Gth+HNhCIjEE6YoeC59ypGqaJ+cZuznuLJ2t3PL+9BQ/2pWXaxhZ5QXFxaJidtzdyebOAgY87/7g3D6PHnWQDg== + version "3.473.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.473.0.tgz#b98c05a12a0c7ef1fe3fbc343829e12e28b0570d" + integrity sha512-JmQdYFJnlBOv+IKlgkTWLNr58FBBgW5dpRzwsraAp/sy6QcPEFKzf48iOvB9ekvLafpHah+7AaFJ3VptLLBoJw== dependencies: "@aws-crypto/sha1-browser" "3.0.0" "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/client-sts" "3.470.0" + "@aws-sdk/client-sts" "3.473.0" "@aws-sdk/core" "3.468.0" "@aws-sdk/credential-provider-node" "3.470.0" "@aws-sdk/middleware-bucket-endpoint" "3.470.0" @@ -330,13 +330,13 @@ tslib "^2.5.0" "@aws-sdk/client-ses@^3.312.0": - version "3.470.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-ses/-/client-ses-3.470.0.tgz#04a708816f1b9e41f4374e2252ff24268faa1a7d" - integrity sha512-jf9MGfahQUfliiOnOXgXtzYVfAY9F0+jKsPtidBRIDEm2829oyOt5K+wuUKxSrb1vFFE3OadC1JRSHs4bMxzAQ== + version "3.473.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-ses/-/client-ses-3.473.0.tgz#38ef2d74c46b0ba770008a1c234aa43be5934ee0" + integrity sha512-oE3e3PIAlQV+9vhk+ALNPZS84C+xgOtUQ9FHXjmMJBNGvQbJclxRquqjm/7AxADlOrzaX8YmPAUSxyrY3FqSxg== dependencies: "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/client-sts" "3.470.0" + "@aws-sdk/client-sts" "3.473.0" "@aws-sdk/core" "3.468.0" "@aws-sdk/credential-provider-node" "3.470.0" "@aws-sdk/middleware-host-header" "3.468.0" @@ -377,13 +377,13 @@ tslib "^2.5.0" "@aws-sdk/client-sqs@^3.345.0": - version "3.470.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sqs/-/client-sqs-3.470.0.tgz#fb7f3e23df39661459293ffc02286500c7d7d332" - integrity sha512-sf86MWKqIABBfpcCLmkMHRXo93WwN8HJBcAvlQ6nPtfiU3VlkkYXF4HvTk1z7/e04x+hZoF/yt3lgFljIjE6Ng== + version "3.473.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sqs/-/client-sqs-3.473.0.tgz#904d21f946f09c521fd8d6530acf4dce5b4b7ce9" + integrity sha512-gf4MXCvgIs4pBol40lWwkJ3qF6UnWOSOkJq8ySMAaRoVl2BJAJCILNZDdRWs4OIyhJIxBJ8fBkZxZCQY82s12A== dependencies: "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/client-sts" "3.470.0" + "@aws-sdk/client-sts" "3.473.0" "@aws-sdk/core" "3.468.0" "@aws-sdk/credential-provider-node" "3.470.0" "@aws-sdk/middleware-host-header" "3.468.0" @@ -465,10 +465,10 @@ "@smithy/util-utf8" "^2.0.2" tslib "^2.5.0" -"@aws-sdk/client-sts@3.470.0": - version "3.470.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.470.0.tgz#f161d087564f9e545fbcd199c7197ec0cfce29b9" - integrity sha512-TP3A4t8FoFEQinm6axxduTUnlMMLpmLi4Sf00JTI2CszxLUFh/JyUhYQ5gSOoXgPFmfwVXUNKCtmR3jdP0ZGPw== +"@aws-sdk/client-sts@3.473.0": + version "3.473.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sts/-/client-sts-3.473.0.tgz#413aaa82e30b6bd49d783f85af95357a89d2a792" + integrity sha512-ttRZs+sW96cpuoVdys4KZ81yXq4c6xyhGOZIRUpi/YiwB1gnNvCEo5CDFL7PSdW/bjI2ovyUgu8EArq+7KlLwA== dependencies: "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" @@ -519,12 +519,12 @@ "@smithy/smithy-client" "^2.1.18" tslib "^2.5.0" -"@aws-sdk/credential-provider-cognito-identity@3.470.0": - version "3.470.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.470.0.tgz#3ea92d14483de7ebe95265850f129a3066aa766a" - integrity sha512-c0YtiBbg4z/4iLnn3gtUtGKBZMQLRk79LjzCN6x98MpIsRTeEBL+4BHYNwFb8C+S4wVDYh2OWqjw1Su6Ues3Wg== +"@aws-sdk/credential-provider-cognito-identity@3.473.0": + version "3.473.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.473.0.tgz#1f19dc9e8542f6999b8735a7b4e3f818206a298a" + integrity sha512-Z5wVssSTQMr3oupl1V3ENibviG99pmKM1QZE2XgECfhyfwfWdRALtCzs+Op6/q+p1zai24seFUZJw7Cl1woKGQ== dependencies: - "@aws-sdk/client-cognito-identity" "3.470.0" + "@aws-sdk/client-cognito-identity" "3.473.0" "@aws-sdk/types" "3.468.0" "@smithy/property-provider" "^2.0.0" "@smithy/types" "^2.7.0" @@ -622,15 +622,15 @@ "@smithy/types" "^2.7.0" tslib "^2.5.0" -"@aws-sdk/credential-providers@3.470.0": - version "3.470.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/credential-providers/-/credential-providers-3.470.0.tgz#e19959b0c9c37a3da666e202ffb964c04ba1ecad" - integrity sha512-aCI/z6L+LwPSUHTsf27WMs3Z7Dfg1idgEOtf0dCkk+T1SZnJA0D/JS0KjQag9rIuqYQsxewx6RCIHus5WJ3czA== +"@aws-sdk/credential-providers@3.473.0": + version "3.473.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-providers/-/credential-providers-3.473.0.tgz#abeafd0caebf25b68176e1b27b2a17bec2aa7a1a" + integrity sha512-SLDgqcSNUeLqXJghDenbpVJ7oFnGyvxC/VJVRGiRwcXNwVn2NB0mMvGtL2/VH/U/leFpZLzouM9VBdHbt2gX3w== dependencies: - "@aws-sdk/client-cognito-identity" "3.470.0" + "@aws-sdk/client-cognito-identity" "3.473.0" "@aws-sdk/client-sso" "3.470.0" - "@aws-sdk/client-sts" "3.470.0" - "@aws-sdk/credential-provider-cognito-identity" "3.470.0" + "@aws-sdk/client-sts" "3.473.0" + "@aws-sdk/credential-provider-cognito-identity" "3.473.0" "@aws-sdk/credential-provider-env" "3.468.0" "@aws-sdk/credential-provider-http" "3.468.0" "@aws-sdk/credential-provider-ini" "3.470.0" @@ -789,13 +789,13 @@ tslib "^2.5.0" "@aws-sdk/rds-signer@^3.315.0": - version "3.470.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/rds-signer/-/rds-signer-3.470.0.tgz#04ed41bd27faf84b30b129b4cb9cc0143732099c" - integrity sha512-j1hjI9J/Ba+V3T9dtv/v2hJ4rYUz2uGQnVUSSmeLtt/nJPyNZzWMHxE0w4UsCGrF5Jw6CdYcYPJ6jVPon8vi1A== + version "3.473.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/rds-signer/-/rds-signer-3.473.0.tgz#592d9c7a6f59c06bc20210d6741a9b3a00bf6bb6" + integrity sha512-brXQ+bH6gJhO8uGxFWVgVoAOXE9U9nAHSKS3QbDfYC+kuiDEIhtJWeV/5xZTkv6XJSjwCpTC0mnNOKusZCwNcQ== dependencies: "@aws-crypto/sha256-browser" "3.0.0" "@aws-crypto/sha256-js" "3.0.0" - "@aws-sdk/credential-providers" "3.470.0" + "@aws-sdk/credential-providers" "3.473.0" "@aws-sdk/util-format-url" "3.468.0" "@smithy/config-resolver" "^2.0.21" "@smithy/hash-node" "^2.0.17" @@ -818,9 +818,9 @@ tslib "^2.5.0" "@aws-sdk/s3-request-presigner@^3.312.0": - version "3.472.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.472.0.tgz#9da9a791f62e4bc1b85dbdaed0511e19da8d88ca" - integrity sha512-BynrM9F1Wn2qdllJQPQR/ilkf/RZTmNnWdPrjHsA8n26UL/O2qt5zvPlx/uRF+2kqN5jR+AeRjik84/gBU7xVQ== + version "3.473.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.473.0.tgz#45ba8660dcf183bb4b4d7cab471a7eeb0ef3a533" + integrity sha512-qQ6i7jGxSlZIunGbrJkrOJR/DEIdKI4CFCl2jjM91jIZatgtk32fIXvViLORklJcqNx+/MhpUUvU39GgsG34iw== dependencies: "@aws-sdk/signature-v4-multi-region" "3.470.0" "@aws-sdk/types" "3.468.0" @@ -2386,6 +2386,26 @@ debug "^3.1.0" lodash.once "^4.1.1" +"@datadog/browser-core@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@datadog/browser-core/-/browser-core-5.5.0.tgz#9de07fd46fbabbce0a30368394f612512869053c" + integrity sha512-orkbetqiXBrF3r9/WImwJG32tEtNjx2hfP7cyBvPw5qhdjOHkl87IdrKqELaadIAc7Tjgmn38TWccSmcRa1rVw== + +"@datadog/browser-rum-core@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@datadog/browser-rum-core/-/browser-rum-core-5.5.0.tgz#fffb23643cd8be62f12e9781024a3f3fda09e1f9" + integrity sha512-VWI91gwKYTGMycpN5a8kgJ/YCdcnvYAhU2uk6HbTLNg2xj4368FyOdSFOQgQzBvmM323AFS+G0v5u+eQgtNgfQ== + dependencies: + "@datadog/browser-core" "5.5.0" + +"@datadog/browser-rum@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@datadog/browser-rum/-/browser-rum-5.5.0.tgz#b677b5684773db11a24c8b8dec3b0674416f625a" + integrity sha512-nsK9hrD4yuyiSJ2B+R0KVeb61Xj4rwVIIF6mGsNYIaX4sN51Qw/lz5GkOca3YwskS0zMoyicjEWKFZnzq5Q7ew== + dependencies: + "@datadog/browser-core" "5.5.0" + "@datadog/browser-rum-core" "5.5.0" + "@datadog/native-appsec@4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-4.0.0.tgz#ee08138b987dec557eac3650a43a972dac85b6a6"