Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into by/onboarding-cypress
Browse files Browse the repository at this point in the history
  • Loading branch information
Bianca Yang committed Mar 18, 2024
2 parents c8764f6 + 3973f21 commit 118a884
Show file tree
Hide file tree
Showing 636 changed files with 3,441 additions and 2,216 deletions.
6 changes: 5 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
<!-- If there are frontend changes, please include screenshots. -->
<!-- If a reference design was involved, include a link to the relevant Figma frame! -->

👉 *Stay up-to-date with [PostHog coding conventions](https://posthog.com/docs/contribute/coding-conventions) for a smoother review.*
👉 _Stay up-to-date with [PostHog coding conventions](https://posthog.com/docs/contribute/coding-conventions) for a smoother review._

## Does this work well for both Cloud and self-hosted?

<!-- Yes / no / it doesn't have an impact. -->

## How did you test this code?

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/build-hogql-parser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:

- name: Check if hogql_parser/ has changed
id: changed-files
uses: tj-actions/changed-files@v42
uses: tj-actions/changed-files@v43
with:
since_last_remote_commit: true
files_yaml: |
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:
python-version: '3.11'

- if: ${{ endsWith(matrix.os, '-arm') }}
uses: deadsnakes/action@v3.0.1 # Unfortunately actions/setup-python@v4 just doesn't work on ARM! This does
uses: deadsnakes/action@v3.1.0 # Unfortunately actions/setup-python@v4 just doesn't work on ARM! This does
with:
python-version: '3.11'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/container-images-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:

- name: Check if any Dockerfile has changed
id: changed-files
uses: tj-actions/changed-files@v42
uses: tj-actions/changed-files@v43
with:
files: |
**/Dockerfile
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
if: ${{ github.repository == 'PostHog/posthog' }}
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v6
- uses: actions/stale@v9
with:
days-before-issue-stale: 730
days-before-issue-close: 14
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/storybook-chromatic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ jobs:
VARIANT: ${{ github.event.pull_request.head.repo.full_name == github.repository && 'update' || 'verify' }}
STORYBOOK_SKIP_TAGS: 'test-skip,test-skip-${{ matrix.browser }}'
run: |
pnpm test:visual-regression:ci:$VARIANT --browsers ${{ matrix.browser }} --shard ${{ matrix.shard }}/$SHARD_COUNT
pnpm test:visual:ci:$VARIANT --browsers ${{ matrix.browser }} --shard ${{ matrix.shard }}/$SHARD_COUNT
- name: Archive failure screenshots
if: ${{ failure() }}
Expand Down
75 changes: 41 additions & 34 deletions .storybook/test-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,27 @@ import { StoryContext } from '@storybook/csf'

// 'firefox' is technically supported too, but as of June 2023 it has memory usage issues that make is unusable
type SupportedBrowserName = 'chromium' | 'webkit'
type SnapshotTheme = 'legacy' | 'light' | 'dark'
type SnapshotTheme = 'light' | 'dark'

// Extend Storybook interface `Parameters` with Chromatic parameters
declare module '@storybook/types' {
interface Parameters {
options?: any
/** @default 'padded' */
layout?: 'padded' | 'fullscreen' | 'centered'
testOptions?: {
/**
* Whether we should wait for all loading indicators to disappear before taking a snapshot.
* @default true
*/
waitForLoadersToDisappear?: boolean
/** If set, we'll wait for the given selector to be satisfied. */
waitForSelector?: string
/** If set, we'll wait for the given selector (or all selectors, if multiple) to be satisfied. */
waitForSelector?: string | string[]
/**
* Whether navigation (sidebar + topbar) should be excluded from the snapshot.
* Warning: Fails if enabled for stories in which navigation is not present.
* Whether navigation should be included in the snapshot. Only applies to `layout: 'fullscreen'` stories.
* @default false
*/
excludeNavigationFromSnapshot?: boolean
includeNavigationInSnapshot?: boolean
/**
* The test will always run for all the browers, but snapshots are only taken in Chromium by default.
* Override this to take snapshots in other browsers too.
Expand All @@ -48,13 +48,13 @@ declare module '@storybook/types' {
}
}

const RETRY_TIMES = 3
const RETRY_TIMES = 2
const LOADER_SELECTORS = [
'.ant-skeleton',
'.Spinner',
'.LemonSkeleton',
'.LemonTableLoader',
'.Toastify__toast-container',
'.Toastify__toast',
'[aria-busy="true"]',
'.SessionRecordingPlayer--buffering',
'.Lettermark--unknown',
Expand All @@ -65,13 +65,26 @@ const customSnapshotsDir = `${process.cwd()}/frontend/__snapshots__`
const JEST_TIMEOUT_MS = 15000
const PLAYWRIGHT_TIMEOUT_MS = 10000 // Must be shorter than JEST_TIMEOUT_MS

const ATTEMPT_COUNT_PER_ID: Record<string, number> = {}

module.exports = {
setup() {
expect.extend({ toMatchImageSnapshot })
jest.retryTimes(RETRY_TIMES, { logErrorsBeforeRetry: true })
jest.setTimeout(JEST_TIMEOUT_MS)
},
async postVisit(page, context) {
ATTEMPT_COUNT_PER_ID[context.id] = (ATTEMPT_COUNT_PER_ID[context.id] || 0) + 1
await page.evaluate(
([retry, id]) => console.log(`[${id}] Attempt ${retry}`),
[ATTEMPT_COUNT_PER_ID[context.id], context.id]
)
if (ATTEMPT_COUNT_PER_ID[context.id] > 1) {
// When retrying, resize the viewport and then resize again to default,
// just in case the retry is due to a useResizeObserver fail
await page.setViewportSize({ width: 1920, height: 1080 })
await page.setViewportSize({ width: 1280, height: 720 })
}
const browserContext = page.context()
const storyContext = await getStoryContext(page, context)
const { snapshotBrowsers = ['chromium'] } = storyContext.parameters?.testOptions ?? {}
Expand All @@ -96,7 +109,7 @@ async function expectStoryToMatchSnapshot(
const {
waitForLoadersToDisappear = true,
waitForSelector,
excludeNavigationFromSnapshot = false,
includeNavigationInSnapshot = false,
} = storyContext.parameters?.testOptions ?? {}

let check: (
Expand All @@ -107,26 +120,29 @@ async function expectStoryToMatchSnapshot(
targetSelector?: string
) => Promise<void>
if (storyContext.parameters?.layout === 'fullscreen') {
if (excludeNavigationFromSnapshot) {
check = expectStoryToMatchSceneSnapshot
if (includeNavigationInSnapshot) {
check = expectStoryToMatchViewportSnapshot
} else {
check = expectStoryToMatchFullPageSnapshot
check = expectStoryToMatchSceneSnapshot
}
} else {
check = expectStoryToMatchComponentSnapshot
}

await waitForPageReady(page)
await page.evaluate(() => {
// Stop all animations for consistent snapshots
await page.evaluate((layout: string) => {
// Stop all animations for consistent snapshots, and adjust other styles
document.body.classList.add('storybook-test-runner')
})
document.body.classList.add(`storybook-test-runner--${layout}`)
}, storyContext.parameters?.layout || 'padded')
if (waitForLoadersToDisappear) {
// The timeout is reduced so that we never allow toasts – they usually signify something wrong
await page.waitForSelector(LOADER_SELECTORS.join(','), { state: 'detached', timeout: 1000 })
await page.waitForSelector(LOADER_SELECTORS.join(','), { state: 'detached', timeout: 3000 })
}
if (waitForSelector) {
if (typeof waitForSelector === 'string') {
await page.waitForSelector(waitForSelector)
} else if (Array.isArray(waitForSelector)) {
await Promise.all(waitForSelector.map((selector) => page.waitForSelector(selector)))
}

await page.waitForTimeout(400) // Wait for effects to finish
Expand All @@ -151,7 +167,7 @@ async function expectStoryToMatchSnapshot(
await check(page, context, browser, 'dark', storyContext.parameters?.testOptions?.snapshotTargetSelector)
}

async function expectStoryToMatchFullPageSnapshot(
async function expectStoryToMatchViewportSnapshot(
page: Page,
context: TestContext,
browser: SupportedBrowserName,
Expand All @@ -166,12 +182,10 @@ async function expectStoryToMatchSceneSnapshot(
browser: SupportedBrowserName,
theme: SnapshotTheme
): Promise<void> {
await page.evaluate(() => {
// The screenshot gets clipped by overflow hidden on .Navigation3000
document.querySelector('Navigation3000')?.setAttribute('style', 'overflow: visible;')
})

await expectLocatorToMatchStorySnapshot(page.locator('main'), context, browser, theme)
// If the `main` element isn't present, let's use `body` - this is needed in logged-out screens.
// We use .last(), because the order of selector matches is based on the order of elements in the DOM,
// and not the order of the selectors in the query.
await expectLocatorToMatchStorySnapshot(page.locator('body, main').last(), context, browser, theme)
}

async function expectStoryToMatchComponentSnapshot(
Expand All @@ -181,13 +195,11 @@ async function expectStoryToMatchComponentSnapshot(
theme: SnapshotTheme,
targetSelector: string = '#storybook-root'
): Promise<void> {
await page.evaluate((theme) => {
await page.evaluate(() => {
const rootEl = document.getElementById('storybook-root')
if (!rootEl) {
throw new Error('Could not find root element')
}
// Make the root element (which is the default screenshot reference) hug the component
rootEl.style.display = 'inline-block'
// If needed, expand the root element so that all popovers are visible in the screenshot
document.querySelectorAll('.Popover').forEach((popover) => {
const currentRootBoundingClientRect = rootEl.getBoundingClientRect()
Expand All @@ -205,9 +217,7 @@ async function expectStoryToMatchComponentSnapshot(
rootEl.style.width = `${-popoverBoundingClientRect.left + currentRootBoundingClientRect.right}px`
}
})
// For legacy style, make the body transparent to take the screenshot without background
document.body.style.background = theme === 'legacy' ? 'transparent' : 'var(--bg-3000)'
}, theme)
})

await expectLocatorToMatchStorySnapshot(page.locator(targetSelector), context, browser, theme, {
omitBackground: true,
Expand All @@ -222,10 +232,7 @@ async function expectLocatorToMatchStorySnapshot(
options?: LocatorScreenshotOptions
): Promise<void> {
const image = await locator.screenshot({ ...options })
let customSnapshotIdentifier = context.id
if (theme !== 'legacy') {
customSnapshotIdentifier += `--${theme}`
}
let customSnapshotIdentifier = `${context.id}--${theme}`
if (browser !== 'chromium') {
customSnapshotIdentifier += `--${browser}`
}
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
<a href="https://posthog.com/docs">Docs</a> - <a href="https://posthog.com/community">Community</a> - <a href="https://posthog.com/roadmap">Roadmap</a> - <a href="https://posthog.com/changelog">Changelog</a> - <a href="https://github.com/PostHog/posthog/issues/new?assignees=&labels=bug&template=bug_report.md">Bug reports</a>
</p>

<p align="center">
<a href="https://www.youtube.com/watch?v=2jQco8hEvTI">
<img src="https://img.youtube.com/vi/2jQco8hEvTI/0.jpg" alt="PostHog Demonstration">
</a>
<em>See PostHog in action</em>
</p>

## PostHog is an all-in-one, open source platform for building better products

- Specify events manually, or use autocapture to get started quickly
Expand Down Expand Up @@ -65,7 +72,7 @@ PostHog brings all the tools and data you need to build better products.

### Analytics and optimization tools

- **Event-based analytics:** Capture your product's usage [automatically](https://posthog.com/docs/integrate/client/js#autocapture), or [customize](https://posthog.com/docs/integrate) it to your needs
- **Event-based analytics:** Capture your product's usage [automatically](https://posthog.com/docs/libraries/js#autocapture), or [customize](https://posthog.com/docs/getting-started/install) it to your needs
- **User and group tracking:** Understand the [people](https://posthog.com/manual/persons) and [groups](https://posthog.com/manual/group-analytics) behind the events and track properties about them
- **Data visualizations:** Create and share [graphs](https://posthog.com/docs/features/trends), [funnels](https://posthog.com/docs/features/funnels), [paths](https://posthog.com/docs/features/paths), [retention](https://posthog.com/docs/features/retention), and [dashboards](https://posthog.com/docs/features/dashboards)
- **SQL access:** Use [SQL](https://posthog.com/docs/product-analytics/sql) to get a deeper understanding of your users, breakdown information and create completely tailored visualizations
Expand Down
57 changes: 57 additions & 0 deletions cypress/e2e/billingUpgradeCTA.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { decideResponse } from '../fixtures/api/decide'
import * as fflate from 'fflate'

// Mainly testing to make sure events are fired as expected

describe('Billing Upgrade CTA', () => {
beforeEach(() => {
cy.intercept('**/decide/*', (req) =>
req.reply(
decideResponse({
'billing-upgrade-language': 'credit_card',
})
)
)

cy.intercept('/api/billing-v2/', { fixture: 'api/billing-v2/billing-v2-unsubscribed.json' })
})

it('Check that events are being sent on each page visit', () => {
cy.visit('/organization/billing')
cy.get('[data-attr=product_analytics-upgrade-cta] .LemonButton__content').should('have.text', 'Add credit card')
cy.window().then((win) => {
const events = (win as any)._cypress_posthog_captures

const matchingEvents = events.filter((event) => event.event === 'billing CTA shown')
// One for each product card
expect(matchingEvents.length).to.equal(4)
})

// Mock billing response with subscription
cy.intercept('/api/billing-v2/', { fixture: 'api/billing-v2/billing-v2.json' })
cy.reload()

cy.get('[data-attr=session_replay-upgrade-cta] .LemonButton__content').should('have.text', 'Add paid plan')
cy.intercept('POST', '**/e/?compression=gzip-js*').as('capture3')
cy.window().then((win) => {
const events = (win as any)._cypress_posthog_captures

const matchingEvents = events.filter((event) => event.event === 'billing CTA shown')
expect(matchingEvents.length).to.equal(4)
})

cy.intercept('/api/billing-v2/', { fixture: 'api/billing-v2/billing-v2-unsubscribed.json' })
// Navigate to the onboarding billing step
cy.visit('/products')
cy.get('[data-attr=product_analytics-onboarding-card]').click()
cy.get('[data-attr=onboarding-breadcrumbs] > :nth-child(5)').click()

cy.intercept('POST', '**/e/?compression=gzip-js*').as('capture4')
cy.window().then((win) => {
const events = (win as any)._cypress_posthog_captures

const matchingEvents = events.filter((event) => event.event === 'billing CTA shown')
expect(matchingEvents.length).to.equal(3)
})
})
})
2 changes: 1 addition & 1 deletion cypress/e2e/events.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ describe('Events', () => {
cy.get('[data-attr="new-prop-filter-EventPropertyFilters.0"]').click()
cy.get('[data-attr=taxonomic-filter-searchfield]').click()
cy.get('[data-attr=prop-filter-event_properties-0]').click()
cy.get('[data-attr=prop-val] .ant-select-selector').click({ force: true })
cy.get('[data-attr=prop-val] .LemonInput').click({ force: true })
cy.wait('@getBrowserValues').then(() => {
cy.get('[data-attr=prop-val-0]').click()
cy.get('.DataTable').should('exist')
Expand Down
6 changes: 3 additions & 3 deletions cypress/e2e/invites.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('Invite Signup', () => {

cy.location('pathname').should('contain', '/settings/organization')
cy.get('[id="invites"]').should('exist')
cy.contains('Pending Invites').should('exist')
cy.contains('Pending invites').should('exist')

// Test invite creation flow
cy.get('[data-attr=invite-teammate-button]').click()
Expand Down Expand Up @@ -102,15 +102,15 @@ describe('Invite Signup', () => {

// Change membership level
cy.contains('[data-attr=org-members-table] tr', user).within(() => {
cy.get('[data-attr=membership-level]').last().should('contain', 'member')
cy.get('[data-attr=membership-level]').last().should('contain', 'Member')
cy.get('[data-attr=more-button]').last().click()
})

// more menu is not within the row
cy.get('[data-test-level=8]').click()

cy.contains('[data-attr=org-members-table] tr', user).within(() => {
cy.get('[data-attr=membership-level]').last().should('contain', 'admin')
cy.get('[data-attr=membership-level]').last().should('contain', 'Admin')
})

// Delete member
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/surveys.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('Surveys', () => {
// select the first property
cy.get('[data-attr="property-select-toggle-0"]').click()
cy.get('[data-attr="prop-filter-person_properties-0"]').click()
cy.get('[data-attr=prop-val] .ant-select-selector').click({ force: true })
cy.get('[data-attr=prop-val] .LemonInput').click({ force: true })
cy.get('[data-attr=prop-val-0]').click({ force: true })

cy.get('[data-attr="rollout-percentage"]').type('100')
Expand Down
8 changes: 0 additions & 8 deletions cypress/productAnalytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,14 +215,6 @@ export const dashboard = {
cy.get('[data-attr="prop-val-0"]').click({ force: true })
cy.get('.PropertyFilterButton').should('have.length', 1)
},
addPropertyFilter(type: string = 'Browser', value: string = 'Chrome'): void {
cy.get('.PropertyFilterButton').should('have.length', 0)
cy.get('[data-attr="property-filter-0"]').click()
cy.get('[data-attr="taxonomic-filter-searchfield"]').click().type('Browser').wait(1000)
cy.get('[data-attr="prop-filter-event_properties-0"]').click({ force: true })
cy.get('.ant-select-selector').type(value)
cy.get('.ant-select-item-option-content').click({ force: true })
},
}

export function createInsight(insightName: string): void {
Expand Down
Loading

0 comments on commit 118a884

Please sign in to comment.