Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export IDP_BASE_URL_PREVIEW=http://localhost:3000
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ node_modules/
.DS_Store
.vscode
.idea
.env

# pulled dynamically in the build/deploy process
_data/team.yml
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -117,6 +119,7 @@ DEPENDENCIES
activesupport
html-proofer
jekyll (~> 4)
jekyll-environment-variables
jekyll-last-modified-at
jekyll-redirect-from
kramdown (>= 2.3.0)
Expand Down
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,4 +15,7 @@ test: build
clean:
rm -rf _site

.env:
cp -n .env.example .env

.PHONY: setup clean
35 changes: 35 additions & 0 deletions _articles/analytics-events.md
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>
2 changes: 1 addition & 1 deletion _articles/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion _layouts/article.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
204 changes: 204 additions & 0 deletions assets/js/analytics-events.js
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
Copy link
Copy Markdown
Contributor

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:

  • Import from an ESM-upgrading CDN like Skypack or unpkg ?module
  • Use something like Snowpack
    • I was more fond of the v1 behavior, which was mostly minimalistic as a way to create local, ESM-upgraded versions of CJS packages. It's now pretty feature-rich, more than we might need or want.
  • Just use a bundler 🤷 ESBuild could work well, since we don't need much transpilation beyond module bundling, and could let us use JSX/TypeScript.

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 '&nbsp;' 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: The name dom suggests to me that this is a full DOM object (e.g. a JSOM DOM wrapper object), when in-fact it's just an element node. I might have used element or node instead.

}
}

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit/Question: Group shouldn't be necessary?

Suggested change
replace(/( $)/, ''); // fix last line indentation
replace(/ $/, ''); // fix last line indentation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would String#trimEnd have worked here?

Suggested change
replace(/( $)/, ''); // fix last line indentation
trimEnd(); // fix last line indentation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Parens were optional, but turns out trimEnd() removes a trailing newline that I wanted to keep for bracket alignment.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Outer html might not be necessary?

Suggested change
return html`${events.map((event) =>
html`<${Event} ...${event} />`
)}`;
return events.map((event) => html`<${Event} ...${event} />`);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 }) =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same note about outer html, unless I'm mistaken (haven't tested).

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');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not impactful here, but micro-optimization note that getElementById is ~2x faster than querySelector.

https://www.measurethat.net/Benchmarks/Show/2488/0/getelementbyid-vs-queryselector

Suggested change
const sidenav = document.querySelector('#sidenav');
const sidenav = document.getElementById('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));
28 changes: 27 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading