Skip to content

Commit

Permalink
feat(exports): Dashboard / Insight exporting (PostHog#9830)
Browse files Browse the repository at this point in the history
* Adds chromium / selenium for image exporting
* Added uploading of downloads folder to artefacts
* Adds ExportButton to generate desired asset
  • Loading branch information
benjackwhite authored May 27, 2022
1 parent e821341 commit 57874f9
Show file tree
Hide file tree
Showing 74 changed files with 1,711 additions and 324 deletions.
1 change: 1 addition & 0 deletions .github/actions/run-backend-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ runs:
touch frontend/dist/index.html
touch frontend/dist/layout.html
touch frontend/dist/shared_dashboard.html
touch frontend/dist/exporter.html
- name: Wait for Clickhouse & Kafka
shell: bash
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci-async-migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ jobs:
touch frontend/dist/index.html
touch frontend/dist/layout.html
touch frontend/dist/shared_dashboard.html
touch frontend/dist/exporter.html
- name: Wait for Clickhouse & Kafka
shell: bash
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ jobs:
touch frontend/dist/index.html
touch frontend/dist/layout.html
touch frontend/dist/shared_dashboard.html
touch frontend/dist/exporter.html
- name: Run cloud tests (posthog-cloud)
run: |
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/docker-image-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ jobs:
with:
push: false
tags: posthog/posthog:testing

- name: Output image info including size
run: |
image_size_bytes=$(docker image inspect ${{ steps.docker_build.outputs.imageid }} | jq -r '.[0].Size')
echo "### Build info" >> $GITHUB_STEP_SUMMARY
echo "Image size: $(jq -n $image_size_bytes/1000000000)GB" >> $GITHUB_STEP_SUMMARY
14 changes: 10 additions & 4 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ env:
SELF_CAPTURE: 0
E2E_TESTING: 1
EMAIL_HOST: 'email.test.posthog.net' # used to test password resets
SITE_URL: 'test.posthog.net' # used to test password resets
SITE_URL: 'http://localhost:8000' # used to test password resets
NO_RESTART_LOOP: 1
CLICKHOUSE_SECURE: 0
OBJECT_STORAGE_ENABLED: 1
Expand Down Expand Up @@ -146,20 +146,26 @@ jobs:
install-command: echo "no"
spec: ${{ matrix.specs }}
- name: Archive test screenshots
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v3
with:
name: screenshots
path: cypress/screenshots
if: ${{ failure() }}
- name: Archive test downloads
uses: actions/upload-artifact@v3
with:
name: downloads
path: cypress/downloads
if: ${{ failure() }}
- name: Archive test videos
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v3
with:
name: videos
path: cypress/videos
if: ${{ failure() }}
- name: Show logs on failure
# use artefact here, as I think the output will be too large for display in an action
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v3
with:
name: logs
path: /tmp/logs
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ yarn-error.log
.yalc
yalc.lock
cypress/screenshots/
cypress/downloads/
cypress/**/*.diff.png
docker-compose.prod.yml
.python-version
*.isorted
Expand Down
5 changes: 2 additions & 3 deletions bin/e2e-test-runner
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
#!/bin/bash
set -e


export DEBUG=1
export NO_RESTART_LOOP=1
export NO_RESTART_LOOP=1
export CYPRESS_BASE_URL=http://localhost:8080
export OPT_OUT_CAPTURE=1
export SECURE_COOKIES=0
Expand All @@ -15,7 +14,7 @@ export CLICKHOUSE_SECURE=0
export E2E_TESTING=1
export SECRET_KEY=e2e_test
export EMAIL_HOST=email.test.posthog.net
export SITE_URL=test.posthog.net
export SITE_URL=http://localhost:8080
export REDIS_URL=redis://localhost/
DATABASE="posthog_e2e_test"
export PGHOST="${PGHOST:=localhost}"
Expand Down
3 changes: 2 additions & 1 deletion cypress.e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"requestTimeout": 8000,
"pageLoadTimeout": 80000,
"projectId": "twojfp",
"viewportWidth": 1200
"viewportWidth": 1200,
"trashAssetsBeforeRuns": true
}
24 changes: 11 additions & 13 deletions cypress/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@


## Testing Feature Flags

The Cypress tests run with a PostHog instance that has no feature flags set up.

To test feature flags you can intercept the call to the `decide` endpoint

```javascript
// sometimes the system under test calls `/decide`
// and sometimes it calls https://app.posthog.com/decide
cy.intercept(/.*\/decide\/.*/, (req) =>
req.reply(
decideResponse({
// add feature flags here, for e.g.
// 'feature-flag-key': true,
})
)
)
```
// sometimes the system under test calls `/decide`
// and sometimes it calls https://app.posthog.com/decide
cy.intercept('https://app.posthog.com/decide/*', (req) =>
req.reply(
decideResponse({
// add feature flags here, for e.g.
// 'feature-flag-key': true,
})
)
)
```
Binary file added cypress/data/exports/export-pageview-count.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion cypress/fixtures/api/decide.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default function decideResponse(featureFlags) {
export function decideResponse(featureFlags) {
return {
config: {
enable_collect_everything: true,
Expand Down
42 changes: 42 additions & 0 deletions cypress/integration/exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { urls } from 'scenes/urls'

import { decideResponse } from '../fixtures/api/decide'

// NOTE: As the API data is randomly generated, we are only really testing here that the overall output is correct
// The actual graph is not under test
describe('Exporting Insights', () => {
beforeEach(() => {
cy.intercept('https://app.posthog.com/decide/*', (req) =>
req.reply(
decideResponse({
'export-dashboard-insights': true,
})
)
)
cy.visit(urls.insightNew())
// apply filter
cy.get('[data-attr=trends-filters-add-filter-group]').click()
cy.get('[data-attr=property-select-toggle-0]').click()
cy.get('[data-attr=taxonomic-filter-searchfield]').click()
cy.get('[data-attr=expand-list-event_properties]').click()
cy.get('[data-attr=prop-filter-event_properties-1]').click({ force: true })
cy.get('[data-attr=prop-val] input').type('not-applicable')
cy.get('[data-attr=prop-val] input').type('{enter}')

// Save
cy.get('[data-attr="insight-save-button"]').click()
})

it('Export an Insight to png', () => {
cy.get('[data-attr=more-button]').click()
cy.get('[data-attr=export-button]').click()
cy.get('[data-attr=export-button-png]').click()

const expecteFileName = 'export-pageview-count.png'
cy.task('compareToReferenceImage', {
source: expecteFileName,
reference: `../data/exports/${expecteFileName}`,
diffThreshold: 0.01,
})
})
})
2 changes: 1 addition & 1 deletion cypress/integration/licenses.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ describe('Licenses', () => {
it('Licenses loaded', () => {
cy.get('[data-attr=top-menu-toggle]').click()
cy.get('[data-attr=top-menu-item-licenses]').click()
cy.get('[data-attr=breadcrumb-0]').should('contain', 'test.posthog.net') // Breadcrumbs work
cy.get('[data-attr=breadcrumb-0]').should('contain', Cypress.config().baseUrl.replace('http://', '')) // Breadcrumbs work
cy.get('[data-attr=breadcrumb-1]').should('have.text', 'Licenses') // Breadcrumbs work
cy.get('h1').should('contain', 'Licenses')
cy.title().should('equal', 'Licenses • PostHog') // Page title works
Expand Down
10 changes: 4 additions & 6 deletions cypress/integration/trends.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('Trends', () => {
cy.get('[data-attr=show-prop-filter-0]').click()
cy.get('[data-attr=property-select-toggle-0]').click()
cy.get('[data-attr="expand-list-event_properties"]').click()
cy.get('.taxonomic-list-row').first().click({ force: true })
cy.get('[data-attr=prop-filter-event_properties-1]').click({ force: true })
cy.get('[data-attr=prop-val]').click({ force: true })
// cypress is odd and even though when a human clicks this the right dropdown opens
// in the test that doesn't happen
Expand All @@ -61,7 +61,6 @@ describe('Trends', () => {
cy.get('.taxonomic-value-select').click()
}
})
cy.get('[data-attr=prop-val-0]').click({ force: true })
cy.get('[data-attr=trend-line-graph]', { timeout: 8000 }).should('exist')
})

Expand All @@ -74,7 +73,7 @@ describe('Trends', () => {
cy.get('[data-attr=property-select-toggle-0]').click()
cy.get('[data-attr=taxonomic-filter-searchfield]').click()
cy.get('[data-attr="expand-list-event_properties"]').click()
cy.get('.taxonomic-list-row').first().click({ force: true })
cy.get('[data-attr=prop-filter-event_properties-1]').click({ force: true })
cy.get('[data-attr=prop-val]').click({ force: true })
// cypress is odd and even though when a human clicks this the right dropdown opens
// in the test that doesn't happen
Expand Down Expand Up @@ -127,7 +126,7 @@ describe('Trends', () => {
it('Apply property breakdown', () => {
cy.get('[data-attr=add-breakdown-button]').click()
cy.get('[data-attr="expand-list-event_properties"]').click()
cy.get('.taxonomic-list-row').first().click()
cy.get('[data-attr=prop-filter-event_properties-1]').click({ force: true })
cy.get('[data-attr=trend-line-graph]').should('exist')
})

Expand All @@ -144,7 +143,7 @@ describe('Trends', () => {
cy.get('[data-attr=property-select-toggle-0]').click()
cy.get('[data-attr=taxonomic-filter-searchfield]').click()
cy.get('[data-attr="expand-list-event_properties"]').click()
cy.get('.taxonomic-list-row').first().click({ force: true })
cy.get('[data-attr=prop-filter-event_properties-1]').click({ force: true })
cy.get('[data-attr=prop-val]').click({ force: true })
// cypress is odd and even though when a human clicks this the right dropdown opens
// in the test that doesn't happen
Expand All @@ -153,7 +152,6 @@ describe('Trends', () => {
cy.get('.taxonomic-value-select').click()
}
})
cy.get('[data-attr=prop-val-0]').click({ force: true })

cy.get('[data-attr=insight-save-button]').click()
cy.get('[data-attr=save-to-dashboard-button]').click()
Expand Down
56 changes: 56 additions & 0 deletions cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
const path = require('path')
const fs = require('fs')
const pixelmatch = require('pixelmatch')
const PNG = require('pngjs').PNG

const downloadDirectory = path.join(__dirname, '..', 'downloads')

const checkFileDownloaded = async (filename, timeout, delayMs = 10) => {
const start = Date.now()
const fullFileName = `${downloadDirectory}/${filename}`

while (Date.now() - start < timeout) {
await new Promise((res) => setTimeout(res, delayMs))

if (fs.existsSync(fullFileName)) {
return fullFileName
}
}
return
}

const webpackPreprocessor = require('@cypress/webpack-preprocessor')

const { createEntry } = require('../../webpack.config')
Expand All @@ -23,5 +44,40 @@ module.exports = (on, config) => {
}
})

on('task', {
compareToReferenceImage({ source, reference, diffThreshold = 0.01, ms = 10000 }) {
return checkFileDownloaded(source, ms).then((fileExists) => {
if (!fileExists) {
return undefined
}

const imgSrc = PNG.sync.read(fs.readFileSync(`${downloadDirectory}/${source}`))
const imgRef = PNG.sync.read(fs.readFileSync(path.join(__dirname, reference)))
const { width, height } = imgSrc
const imgDiff = new PNG({ width, height })

const numDiffPixels = pixelmatch(imgSrc.data, imgRef.data, imgDiff.data, width, height, {
threshold: 0.1,
})

const imgDiffFilename = `${downloadDirectory}/${source}.diff.png`

fs.writeFileSync(imgDiffFilename, PNG.sync.write(imgDiff))

const percentageDiff = numDiffPixels / (width * height)

if (percentageDiff > diffThreshold) {
throw new Error(
`Reference image is off by ${(percentageDiff * 100).toFixed(
2
)}% (${numDiffPixels}) pixels. See ${imgDiffFilename} for more info`
)
}

return true
})
},
})

return config
}
7 changes: 7 additions & 0 deletions dev.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ RUN apk --update --no-cache add \
"make~=4.3" \
"nodejs-current~=16" \
"npm~=7" \
"chromium~=93" \
"chromium-chromedriver~=93" \
&& npm install -g yarn@1

# Compile and install Python dependencies.
Expand Down Expand Up @@ -87,4 +89,9 @@ RUN mkdir -p frontend/dist && \
# Expose container port and run entry point script
EXPOSE 8000
EXPOSE 8234

ENV CHROME_BIN=/usr/bin/chromium-browser \
CHROME_PATH=/usr/lib/chromium/ \
CHROMEDRIVER_BIN=/usr/bin/chromedriver

CMD ["./bin/docker-dev"]
1 change: 1 addition & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ services:
PGHOST: db
PGUSER: posthog
PGPASSWORD: posthog
SITE_URL: http://web:8000
depends_on:
- db
- redis
Expand Down
3 changes: 2 additions & 1 deletion ee/bin/docker-ch-test
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ mkdir -p frontend/dist
touch frontend/dist/index.html
touch frontend/dist/layout.html
touch frontend/dist/shared_dashboard.html
pytest ee
touch frontend/dist/exporter.html
pytest ee
15 changes: 15 additions & 0 deletions frontend/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export function writeSharedDashboardHtml(chunks = {}, entrypoints = []) {
copyIndexHtml('src/shared_dashboard.html', 'dist/shared_dashboard.html', 'shared_dashboard', chunks, entrypoints)
}

export function writeExporterHtml(chunks = {}, entrypoints = []) {
copyIndexHtml('src/exporter.html', 'dist/exporter.html', 'exporter', chunks, entrypoints)
}

startDevServer()
copyPublicFolder()
writeSourceCodeEditorTypes()
Expand All @@ -68,6 +72,13 @@ buildInParallel(
format: 'iife',
outfile: path.resolve(__dirname, 'dist', 'shared_dashboard.js'),
},
{
name: 'Exporter',
entryPoints: ['src/exporter/ExportViewer.tsx'],
bundle: true,
format: 'iife',
outfile: path.resolve(__dirname, 'dist', 'exporter.js'),
},
{
name: 'Toolbar',
entryPoints: ['src/toolbar/index.tsx'],
Expand Down Expand Up @@ -100,6 +111,10 @@ buildInParallel(
writeSharedDashboardHtml(chunks, entrypoints)
}

if (config.name === 'Exporter') {
writeExporterHtml(chunks, entrypoints)
}

createHashlessEntrypoints(entrypoints)

buildsInProgress--
Expand Down
Loading

0 comments on commit 57874f9

Please sign in to comment.