Skip to content

Commit

Permalink
Sending events to POST /events endpoint (#15796)
Browse files Browse the repository at this point in the history
heiskr authored Oct 8, 2020

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
1 parent a30d955 commit 7c0c493
Showing 9 changed files with 804 additions and 276 deletions.
118 changes: 118 additions & 0 deletions javascripts/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/* eslint-disable camelcase */
import { v4 as uuidv4 } from 'uuid'
import Cookies from 'js-cookie'
import getCsrf from './get-csrf'

const COOKIE_NAME = '_docs-events'

let cookieValue

export function getUserEventsId () {
if (cookieValue) return cookieValue
cookieValue = Cookies.get(COOKIE_NAME)
if (cookieValue) return cookieValue
cookieValue = uuidv4()
Cookies.set(COOKIE_NAME, cookieValue, {
secure: true,
sameSite: 'strict',
expires: 365
})
return cookieValue
}

export async function sendEvent ({
type,
version = '1.0.0',
page_render_duration,
exit_page_id,
exit_first_paint,
exit_dom_interactive,
exit_dom_complete,
exit_visit_duration,
exit_scroll_length,
link_url,
search_query,
search_context,
navigate_label,
survey_vote,
survey_comment,
survey_email,
experiment_name,
experiment_variation,
experiment_success
}) {
const response = await fetch('/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': getCsrf()
},
body: JSON.stringify({
type, // One of page, exit, link, search, navigate, survey, experiment

context: {
// Primitives
event_id: uuidv4(),
user: getUserEventsId(),
version,
created: new Date().toISOString(),

// Content information
path: location.pathname,
referrer: document.referrer,
search: location.search,
href: location.href,
site_language: location.pathname.split('/')[1],

// Device information
// os:
// os_version:
// browser:
// browser_version:
viewport_width: document.documentElement.clientWidth,
viewport_height: document.documentElement.clientHeight,

// Location information
timezone: new Date().getTimezoneOffset() / -60,
user_language: navigator.language
},

// Page event
page_render_duration,

// Exit event
exit_page_id,
exit_first_paint,
exit_dom_interactive,
exit_dom_complete,
exit_visit_duration,
exit_scroll_length,

// Link event
link_url,

// Search event
search_query,
search_context,

// Navigate event
navigate_label,

// Survey event
survey_vote,
survey_comment,
survey_email,

// Experiment event
experiment_name,
experiment_variation,
experiment_success
})
})
const data = response.ok ? await response.json() : {}
return data
}

export default async function initializeEvents () {
await sendEvent({ type: 'page' })
}
6 changes: 4 additions & 2 deletions javascripts/index.js
Original file line number Diff line number Diff line change
@@ -14,8 +14,9 @@ import localization from './localization'
import helpfulness from './helpfulness'
import experiment from './experiment'
import { fillCsrf } from './get-csrf'
import initializeEvents from './events'

document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', async () => {
displayPlatformSpecificContent()
explorer()
search()
@@ -27,7 +28,8 @@ document.addEventListener('DOMContentLoaded', () => {
wrapCodeTerms()
print()
localization()
fillCsrf()
await fillCsrf() // this must complete before any POST calls
helpfulness()
experiment()
initializeEvents()
})
23 changes: 20 additions & 3 deletions lib/hydro.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
const crypto = require('crypto')
const fetch = require('node-fetch')

const SCHEMAS = {
page: 'docs.v0.PageEvent',
exit: 'docs.v0.ExitEvent',
link: 'docs.v0.LinkEvent',
search: 'docs.v0.SearchEvent',
navigate: 'docs.v0.NavigateEvent',
survey: 'docs.v0.SurveyEvent',
experiment: 'docs.v0.ExperimentEvent'
}

module.exports = class Hydro {
constructor ({ secret, endpoint }) {
constructor ({ secret, endpoint } = {}) {
this.secret = secret || process.env.HYDRO_SECRET
this.endpoint = endpoint || process.env.HYDRO_ENDPOINT
this.schemas = SCHEMAS
}

/**
@@ -32,7 +43,13 @@ module.exports = class Hydro {
* @param {[{ schema: string, value: any }]} events
*/
async publishMany (events) {
const body = JSON.stringify({ events })
const body = JSON.stringify({
events: events.map(({ schema, value }) => ({
schema,
value: JSON.stringify(value), // We must double-encode the value property
cluster: 'potomac' // We only have ability to publish externally to potomac cluster
}))
})
const token = this.generatePayloadHmac(body)

return fetch(this.endpoint, {
@@ -41,7 +58,7 @@ module.exports = class Hydro {
headers: {
Authorization: `Hydro ${token}`,
'Content-Type': 'application/json',
'X-Hydro-App': 'docs'
'X-Hydro-App': 'docs-production'
}
})
}
373 changes: 208 additions & 165 deletions lib/schema-event-2.js

Large diffs are not rendered by default.

24 changes: 23 additions & 1 deletion middleware/events.js
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ const Airtable = require('airtable')
const { omit } = require('lodash')
const Ajv = require('ajv')
const schema = require('../lib/schema-event')
const schemaHydro = require('../lib/schema-event-2')

const TABLE_NAMES = {
HELPFULNESS: 'Helpfulness Survey',
@@ -15,7 +16,7 @@ const ajv = new Ajv()

const router = express.Router()

router.post('/', async (req, res, next) => {
async function airtablePost (req, res, next) {
const { AIRTABLE_API_KEY, AIRTABLE_BASE_KEY } = process.env
if (!AIRTABLE_API_KEY || !AIRTABLE_BASE_KEY) {
return res.status(501).send({})
@@ -36,6 +37,27 @@ router.post('/', async (req, res, next) => {
console.error('unable to POST event', err)
return res.status(err.statusCode).send(err)
}
}

router.post('/', async (req, res, next) => {
// All-caps type is an "Airtable" event
if (req.body.type === 'HELPFULNESS' || req.body.type === 'EXPERIMENT') {
return airtablePost(req, res, next)
}
// Remove the condition above when we are no longer sending to Airtable
if (!ajv.validate(schemaHydro, req.body)) {
if (process.env.NODE_ENV === 'development') console.log(ajv.errorsText())
return res.status(400).json({})
}
const fields = omit(req.body, OMIT_FIELDS)
try {
const hydroRes = await req.hydro.publish(req.hydro.schemas[req.body.type], fields)
if (!hydroRes.ok) return res.status(500).json({})
return res.status(201).json(fields)
} catch (err) {
if (process.env.NODE_ENV === 'development') console.log(err)
return res.status(500).json({})
}
})

router.put('/:id', async (req, res, next) => {
1 change: 1 addition & 0 deletions middleware/index.js
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ module.exports = function (app) {
app.use(require('./cors'))
app.use(require('./csp'))
app.use(require('helmet')())
app.use(require('./req-utils'))
app.use(require('./robots'))
app.use(require('./cookie-parser'))
app.use(require('./csrf'))
6 changes: 6 additions & 0 deletions middleware/req-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const Hydro = require('../lib/hydro')

module.exports = (req, res, next) => {
req.hydro = new Hydro()
return next()
}
504 changes: 407 additions & 97 deletions tests/rendering/events.js

Large diffs are not rendered by default.

25 changes: 17 additions & 8 deletions tests/unit/hydro.js
Original file line number Diff line number Diff line change
@@ -11,18 +11,22 @@ describe('hydro', () => {
reqheaders: {
Authorization: /^Hydro [\d\w]{64}$/,
'Content-Type': 'application/json',
'X-Hydro-App': 'docs'
'X-Hydro-App': 'docs-production'
}
})
// Respond with a 201 and store the body we sent
.post('/').reply(201, (_, body) => { params = body })
// Respond with a 200 and store the body we sent
.post('/').reply(200, (_, body) => { params = body })
})

describe('#publish', () => {
it('publishes a single event to Hydro', async () => {
await hydro.publish('event-name', { pizza: true })
expect(params).toEqual({
events: [{ schema: 'event-name', value: { pizza: true } }]
events: [{
schema: 'event-name',
value: JSON.stringify({ pizza: true }),
cluster: 'potomac'
}]
})
})
})
@@ -35,10 +39,15 @@ describe('hydro', () => {
])

expect(params).toEqual({
events: [
{ schema: 'event-name', value: { pizza: true } },
{ schema: 'other-name', value: { salad: false } }
]
events: [{
schema: 'event-name',
value: JSON.stringify({ pizza: true }),
cluster: 'potomac'
}, {
schema: 'other-name',
value: JSON.stringify({ salad: false }),
cluster: 'potomac'
}]
})
})
})

0 comments on commit 7c0c493

Please sign in to comment.