diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..b4ff0e40 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +export IDP_BASE_URL_PREVIEW=http://localhost:3000 diff --git a/.gitignore b/.gitignore index f3daba9e..4e19eb59 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,5 @@ node_modules/ .DS_Store .vscode .idea +.env -# pulled dynamically in the build/deploy process -_data/team.yml diff --git a/Gemfile b/Gemfile index e833e03a..5cb7f409 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem "kramdown", ">= 2.3.0" group :jekyll_plugins do gem "jekyll-redirect-from" gem "jekyll-last-modified-at" + gem "jekyll-environment-variables" end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 2e3229df..89793a47 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,6 +46,8 @@ GEM rouge (~> 3.0) safe_yaml (~> 1.0) terminal-table (~> 1.8) + jekyll-environment-variables (1.0.1) + jekyll (>= 3.0, < 5.x) jekyll-last-modified-at (1.3.0) jekyll (>= 3.7, < 5.0) posix-spawn (~> 0.3.9) @@ -117,6 +119,7 @@ DEPENDENCIES activesupport html-proofer jekyll (~> 4) + jekyll-environment-variables jekyll-last-modified-at jekyll-redirect-from kramdown (>= 2.3.0) diff --git a/Makefile b/Makefile index 8199fbf6..1c30836d 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ -run: setup - bundle exec jekyll serve --watch +run: setup .env + /bin/bash -c "source .env && bundle exec jekyll serve --watch" -build: setup - bundle exec jekyll build +build: setup .env + /bin/bash -c "source .env && bundle exec jekyll build" setup: bundle @@ -15,4 +15,7 @@ test: build clean: rm -rf _site +.env: + cp -n .env.example .env + .PHONY: setup clean diff --git a/_articles/analytics-events.md b/_articles/analytics-events.md new file mode 100644 index 00000000..eaf078c7 --- /dev/null +++ b/_articles/analytics-events.md @@ -0,0 +1,35 @@ +--- +title: "Analytics Events" +description: "Event descriptions" +layout: article +category: Reporting +--- + +{% if site.env.BRANCH == 'main' %} + {% assign idp_base_url = site.env.IDP_BASE_URL %} +{% else %} + {% assign idp_base_url = site.env.IDP_BASE_URL_PREVIEW %} +{% endif %} +{% assign idp_base_url = idp_base_url | default: 'https://secure.login.gov' %} + +These are the events that are documented in the IDP. Each event has can have custom +properties that go under `event_properties` in the final payload: + +### Events + +```json +{ + "name": "Event Name", + "user_id": "some-user-id", + "properties": { + "event_properties": {} + } +} +``` + +
+
+ + \ No newline at end of file diff --git a/_articles/queries.md b/_articles/queries.md index 26f32e71..20a7e394 100644 --- a/_articles/queries.md +++ b/_articles/queries.md @@ -2,7 +2,7 @@ title: "Reporting Queries" description: "Queries to run in the Rails console for common reporting questions" layout: article -category: "AppDev" +category: "Reporting" --- ## Query timeout diff --git a/_config.yml b/_config.yml index 79aba856..13b5849b 100644 --- a/_config.yml +++ b/_config.yml @@ -15,10 +15,13 @@ copy_to_destination: - node_modules/identity-style-guide/dist/assets - node_modules/anchor-js/anchor.min.js - node_modules/@18f/private-eye/private-eye.js + - node_modules/preact/dist/preact.module.js + - node_modules/htm/dist/htm.module.js plugins: - jekyll-redirect-from - jekyll-last-modified-at + - jekyll-environment-variables exclude: - CONTRIBUTING.md diff --git a/_layouts/article.html b/_layouts/article.html index 4c4d36cf..bc29b616 100644 --- a/_layouts/article.html +++ b/_layouts/article.html @@ -10,7 +10,7 @@ html=content sanitize=true class="inline_toc usa-accordion usa-sidenav" - id="my_toc" + id="sidenav" item_class="usa-sidenav__item" submenu_class="usa-sidenav__sublist" h_min=2 diff --git a/assets/js/analytics-events.js b/assets/js/analytics-events.js new file mode 100644 index 00000000..bf5cc177 --- /dev/null +++ b/assets/js/analytics-events.js @@ -0,0 +1,204 @@ +import { h, Component, render } from '../../preact.module.js'; +import htm from '../../htm.module.js'; + +// Copied from https://github.com/bryanbraun/anchorjs +const urlify = new function() { + // hax to make the below copy-paste more easily + this.options = { truncate: 64 }; + + /** + * Urlify - Refine text so it makes a good ID. + * + * To do this, we remove apostrophes, replace non-safe characters with hyphens, + * remove extra hyphens, truncate, trim hyphens, and make lowercase. + * + * @param {String} text - Any text. Usually pulled from the webpage element we are linking to. + * @return {String} - hyphen-delimited text for use in IDs and URLs. + */ + this.urlify = function(text) { + // Decode HTML characters such as ' ' first. + var textareaElement = document.createElement('textarea'); + textareaElement.innerHTML = text; + text = textareaElement.value; + + // Regex for finding the non-safe URL characters (many need escaping): + // & +$,:;=?@"#{}|^~[`%!'<>]./()*\ (newlines, tabs, backspace, vertical tabs, and non-breaking space) + var nonsafeChars = /[& +$,:;=?@"#{}|^~[`%!'<>\]./()*\\\n\t\b\v\u00A0]/g; + + // The reason we include this _applyRemainingDefaultOptions is so urlify can be called independently, + // even after setting options. This can be useful for tests or other applications. + if (!this.options.truncate) { + _applyRemainingDefaultOptions(this.options); + } + + // Note: we trim hyphens after truncating because truncating can cause dangling hyphens. + // Example string: // " ⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." + return text.trim() // "⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." + .replace(/'/gi, '') // "⚡⚡ Dont forget: URL fragments should be i18n-friendly, hyphenated, short, and clean." + .replace(nonsafeChars, '-') // "⚡⚡-Dont-forget--URL-fragments-should-be-i18n-friendly--hyphenated--short--and-clean-" + .replace(/-{2,}/g, '-') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-short-and-clean-" + .substring(0, this.options.truncate) // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-" + .replace(/^-+|-+$/gm, '') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated" + .toLowerCase(); // "⚡⚡-dont-forget-url-fragments-should-be-i18n-friendly-hyphenated" + }; + + // hax to make the above copy-paste more easily + this.urlify = this.urlify.bind(this); +}().urlify; + +window.urlify = urlify; + +const html = htm.bind(h); + +function Anchor({ slug, icon = String.fromCharCode(59851) }) { + const setRef = (dom) => { + if (dom && document.location.hash.slice(1) === slug) { + setTimeout(() => dom.scrollIntoView(), 0); + } + } + + return html` + `; +} + +function Example({ event_name, attributes }) { + function typeExample(type) { + switch(type) { + case 'Boolean': + return ['true', 'false']; + case 'Integer': + return 0; + case 'Hash': + return '{}'; + default: + return type; + } + } + + const attributeExamples = attributes.flatMap(({ name, types, description }) => { + const value = types.flatMap((type) => typeExample(type)).join(' | '); + + let example = [`${name}: ${value},`]; + + if (description) { + example.unshift(`// ${description}`); + } + + return example; + }); + + if (attributeExamples.length) { + attributeExamples.unshift(''); + attributeExamples.push(''); + } + + const eventProperties = attributeExamples. + join("\n "). + replace(/( $)/, ''); // fix last line indentation + + const example = `{ + name: ${JSON.stringify(event_name)}, + properties: { + event_properties: {${eventProperties}} + } +}`; + + return html`
${example}
`; +} + +function Attribute({ name, types, description }) { + return html` +
  • + ${name} + ${ types?.length ? html`${' '}(${types.join(', ')})` : undefined } +

    ${description}

    +
  • + `; +} + +function Event({ event_name, description, attributes = [] }) { + return html` +
    +

    + ${event_name} + <${Anchor} slug=${urlify(event_name)} /> +

    +

    ${description}

    + + ${ + attributes?.length ? html` +

    + Attributes + <${Anchor} slug=${urlify(event_name + ' Attributes')} /> +

    +
    + Show attribute details + +
    + ` : undefined + } + +

    + Example + <${Anchor} slug=${urlify(event_name + ' Example')} /> +

    + <${Example} event_name=${event_name} attributes=${attributes} /> +
    + `; +} + +function Events({ events }) { + return html`${events.map((event) => + html`<${Event} ...${event} />` + )}`; +} + +function SidebarNavItem({ name }) { + return html` +
  • + ${name} +
  • + `; +} + +function ErrorPage({ error, url }) { + return html` +
    +
    +
    Error loading event definitions
    +
    +

    There was an error loading event definitions from ${url}:

    +

    ${error.message}

    +
    +
    +
    + ` +} + +function Sidenav({ events }) { + return html`${events.map(({ event_name: name }) => + html`<${SidebarNavItem} name=${name} />` + )}`; +} + +const container = document.querySelector('#events-container'); +const { idpBaseUrl } = container.dataset; +const eventsUrl = `${idpBaseUrl}/api/analytics-events`; + +const sidenav = document.querySelector('#sidenav'); + +window.fetch(eventsUrl) + .then((response) => response.json()) + .then(({ events }) => { + render(html`<${Events} events=${events} />`, container); + render(html`<${Sidenav} events=${events} />`, sidenav); + }) + .catch((error) => render(html`<${ErrorPage} url=${eventsUrl} error=${error} />`, container)); diff --git a/package-lock.json b/package-lock.json index b2b71b4e..1c5e9b2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,9 @@ "dependencies": { "@18f/private-eye": "^2.0.0", "anchor-js": "^4.2.2", - "identity-style-guide": "^6.3.0" + "htm": "^3.1.0", + "identity-style-guide": "^6.3.0", + "preact": "^10.6.6" } }, "node_modules/@18f/private-eye": { @@ -38,6 +40,11 @@ "node": ">=4.0.0" } }, + "node_modules/htm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz", + "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q==" + }, "node_modules/identity-style-guide": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/identity-style-guide/-/identity-style-guide-6.3.0.tgz", @@ -69,6 +76,15 @@ "node": ">=0.10.0" } }, + "node_modules/preact": { + "version": "10.6.6", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.6.6.tgz", + "integrity": "sha512-dgxpTFV2vs4vizwKohYKkk7g7rmp1wOOcfd4Tz3IB3Wi+ivZzsn/SpeKJhRENSE+n8sUfsAl4S3HiCVT923ABw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/receptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/receptor/-/receptor-1.0.0.tgz", @@ -127,6 +143,11 @@ "resolved": "https://registry.npmjs.org/element-closest/-/element-closest-2.0.2.tgz", "integrity": "sha1-cqdAoQdFM4LijfnOXbtajfD5Zuw=" }, + "htm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.0.tgz", + "integrity": "sha512-L0s3Sid5r6YwrEvkig14SK3Emmc+kIjlfLhEGn2Vy3bk21JyDEes4MoDsbJk6luaPp8bugErnxPz86ZuAw6e5Q==" + }, "identity-style-guide": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/identity-style-guide/-/identity-style-guide-6.3.0.tgz", @@ -151,6 +172,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "preact": { + "version": "10.6.6", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.6.6.tgz", + "integrity": "sha512-dgxpTFV2vs4vizwKohYKkk7g7rmp1wOOcfd4Tz3IB3Wi+ivZzsn/SpeKJhRENSE+n8sUfsAl4S3HiCVT923ABw==" + }, "receptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/receptor/-/receptor-1.0.0.tgz", diff --git a/package.json b/package.json index 6535c43b..e8af700a 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,8 @@ "dependencies": { "@18f/private-eye": "^2.0.0", "anchor-js": "^4.2.2", - "identity-style-guide": "^6.3.0" + "htm": "^3.1.0", + "identity-style-guide": "^6.3.0", + "preact": "^10.6.6" } }