-
Notifications
You must be signed in to change notification settings - Fork 26
Add article that renders IDP analytics events from JSON (LG-5590) #218
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
Changes from all commits
34b5b67
2d67c51
4bc7d36
7aafb50
7890c52
198f236
ffc5109
1d37649
c3b7a9d
6449cea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export IDP_BASE_URL_PREVIEW=http://localhost:3000 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,5 @@ node_modules/ | |
| .DS_Store | ||
| .vscode | ||
| .idea | ||
| .env | ||
|
|
||
| # pulled dynamically in the build/deploy process | ||
| _data/team.yml | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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": {} | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| <div | ||
| id="events-container" | ||
| data-idp-base-url="{{ idp_base_url }}"> | ||
| </div> | ||
|
|
||
| <script type="module" src="{{ "/assets/js/analytics-events.js" | prepend: site.baseurl }}"></script> |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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); | ||||||||||
|
Comment on lines
+54
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: The name |
||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return html` | ||||||||||
| <a ref=${setRef} | ||||||||||
| class="anchorjs-link" | ||||||||||
| aria-label="Anchor" | ||||||||||
| data-anchorjs-icon=${icon} | ||||||||||
| id=${slug} | ||||||||||
| href=${`#${slug}`} | ||||||||||
| style="font: 1em / 1 anchorjs-icons; padding-left: 0.375em;"></a>`; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function Example({ event_name, attributes }) { | ||||||||||
| function typeExample(type) { | ||||||||||
| switch(type) { | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For future: Would be nice to set up ESLint/Prettier in this project. |
||||||||||
| 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 | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit/Question: Group shouldn't be necessary?
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parens were optional, but turns out There's probably a better way to manage building these fixed-width examples than futzing with indentation and newlines like this |
||||||||||
|
|
||||||||||
| const example = `{ | ||||||||||
| name: ${JSON.stringify(event_name)}, | ||||||||||
| properties: { | ||||||||||
| event_properties: {${eventProperties}} | ||||||||||
| } | ||||||||||
| }`; | ||||||||||
|
|
||||||||||
| return html`<code><pre>${example}</pre></code>`; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function Attribute({ name, types, description }) { | ||||||||||
| return html` | ||||||||||
| <li> | ||||||||||
| <kbd>${name}</kbd> | ||||||||||
| ${ types?.length ? html`${' '}<span>(${types.join(', ')})</span>` : undefined } | ||||||||||
| <p>${description}</p> | ||||||||||
| </li> | ||||||||||
| `; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function Event({ event_name, description, attributes = [] }) { | ||||||||||
| return html` | ||||||||||
| <div> | ||||||||||
| <h3> | ||||||||||
| ${event_name} | ||||||||||
| <${Anchor} slug=${urlify(event_name)} /> | ||||||||||
| </h3> | ||||||||||
| <p>${description}</p> | ||||||||||
|
|
||||||||||
| ${ | ||||||||||
| attributes?.length ? html` | ||||||||||
| <h4> | ||||||||||
| Attributes | ||||||||||
| <${Anchor} slug=${urlify(event_name + ' Attributes')} /> | ||||||||||
| </h4> | ||||||||||
| <details> | ||||||||||
| <summary>Show attribute details</summary> | ||||||||||
| <ul> | ||||||||||
| ${attributes.map((attribute) => html`<${Attribute} ...${attribute} />` )} | ||||||||||
| </ul> | ||||||||||
| </details> | ||||||||||
| ` : undefined | ||||||||||
| } | ||||||||||
|
|
||||||||||
| <h4> | ||||||||||
| Example | ||||||||||
| <${Anchor} slug=${urlify(event_name + ' Example')} /> | ||||||||||
| </h4> | ||||||||||
| <${Example} event_name=${event_name} attributes=${attributes} /> | ||||||||||
| </div> | ||||||||||
| `; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function Events({ events }) { | ||||||||||
| return html`${events.map((event) => | ||||||||||
| html`<${Event} ...${event} />` | ||||||||||
| )}`; | ||||||||||
|
Comment on lines
+159
to
+161
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Outer
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup! this worked |
||||||||||
| } | ||||||||||
|
|
||||||||||
| function SidebarNavItem({ name }) { | ||||||||||
| return html` | ||||||||||
| <li class="usa-sidenav__item"> | ||||||||||
| <a href=${`#${urlify(name)}`}>${name}</a> | ||||||||||
| </li> | ||||||||||
| `; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function ErrorPage({ error, url }) { | ||||||||||
| return html` | ||||||||||
| <div class="usa-alert usa-alert--error"> | ||||||||||
| <div class="usa-alert__body"> | ||||||||||
| <h5 class="usa-alert__heading">Error loading event definitions</h5> | ||||||||||
| <div class="usa-alert__text"> | ||||||||||
| <p>There was an error loading event definitions from <a href=${url}>${url}</a>:</p> | ||||||||||
| <p>${error.message}</p> | ||||||||||
| </div> | ||||||||||
| </div> | ||||||||||
| </div> | ||||||||||
| ` | ||||||||||
| } | ||||||||||
|
|
||||||||||
| function Sidenav({ events }) { | ||||||||||
| return html`${events.map(({ event_name: name }) => | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same note about outer |
||||||||||
| 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'); | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not impactful here, but micro-optimization note that https://www.measurethat.net/Benchmarks/Show/2488/0/getelementbyid-vs-queryselector
Suggested change
|
||||||||||
|
|
||||||||||
| 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)); | ||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While unfortunate 'til more packages ship ESM-native code, this seems okay enough.
A few alternatives that pop to mind, none of which I'm particularly compelled toward:
?module