From b8a506c64d40321d6f365101d89d0e99c4083e2c Mon Sep 17 00:00:00 2001 From: Bianca Yang Date: Fri, 1 Mar 2024 18:00:42 -0800 Subject: [PATCH] iteration 1 --- cypress/e2e/onboarding.cy.ts | 170 ++++++++++++------ .../OnboardingProductIntroduction.tsx | 2 +- .../androidRecordingPromptBannerLogic.ts | 99 ++++++++++ 3 files changed, 214 insertions(+), 57 deletions(-) create mode 100644 frontend/src/scenes/session-recordings/mobile-replay/androidRecordingPromptBannerLogic.ts diff --git a/cypress/e2e/onboarding.cy.ts b/cypress/e2e/onboarding.cy.ts index ded7233ba4457..fd183986c2ee8 100644 --- a/cypress/e2e/onboarding.cy.ts +++ b/cypress/e2e/onboarding.cy.ts @@ -1,8 +1,14 @@ -import { urls } from 'scenes/urls' import { decideResponse } from '../fixtures/api/decide' describe('Onboarding', () => { beforeEach(() => { + cy.intercept('https://us.i.posthog.com/decide/*', (req) => + req.reply( + decideResponse({ + 'product-intro-pages': 'test', + }) + ) + ) cy.intercept('https://app.posthog.com/decide/*', (req) => req.reply( decideResponse({ @@ -12,86 +18,138 @@ describe('Onboarding', () => { ) }) - // it('Navigate between /products to /onboarding to a product intro page', () => { - // cy.visit('/products') + it('Navigate between /products to /onboarding to a product intro page', () => { + cy.visit('/products') + + // Get started on product analytics onboarding + cy.get('[data-attr=product_analytics-onboarding-card]').click() + + // Confirm product intro is not included as the first step in the upper right breadcrumbs + cy.get('[data-attr=onboarding-breadcrumbs] > :first-child > * span').should('not.contain', 'Product Intro') - // Get started on product analytics onboarding - // cy.get('[data-attr=product_analytics-onboarding-card]').click() + // Navigate to the product intro page by clicking the left side bar + cy.get('[data-attr=menu-item-replay').click() - // // Confirm product intro is not included as the first step in the upper right breadcrumbs - // cy.get('[data-attr=onboarding-breadcrumbs] > :first-child > * span').should('not.contain', 'Product Intro') + // Confirm we're on the product_intro page + cy.get('[data-attr=top-bar-name] > span').contains('Product intro') - // // Navigate to the product intro page by clicking the left side bar - // cy.get('[data-attr=menu-item-replay').click() + // Go back to /products + cy.visit('/products') - // // Confirm we're on the product_intro page - // cy.get('[data-attr=top-bar-name] > span').contains('Product intro') + // Again get started on product analytics onboarding + cy.get('[data-attr=product_analytics-get-started-button]').click() - // // Go back to /products - // cy.visit('/products') + // Navigate to the product intro page by changing the url + cy.visit(urls.onboarding('session_replay', 'product_intro')) - // // Again get started on product analytics onboarding - // cy.get('[data-attr=product_analytics-get-started-button]').click() + // Confirm we're on the product intro page + cy.get('[data-attr=top-bar-name] > span').contains('Product intro') + }) + + it('Step through PA onboarding', () => { + cy.visit('/products') - // // Navigate to the product intro page by changing the url - // cy.visit(urls.onboarding('session_replay', 'product_intro')) + // Get started on product analytics onboarding + cy.get('[data-attr=product_analytics-onboarding-card]').click() - // // Confirm we're on the product intro page - // cy.get('[data-attr=top-bar-name] > span').contains('Product intro') - // }) + // Installation should be complete + cy.get('svg.LemonIcon.text-success').should('exist') + cy.get('svg.LemonIcon.text-success').parent().should('contain', 'Installation complete') - // it('Step through PA onboarding', () => { - // cy.visit('/products') + // Continue to configuration step + cy.get('[data-attr=sdk-continue]').click() - // Get started on product analytics onboarding - // cy.get('[data-attr=product_analytics-onboarding-card]').click() + // Confirm the appropriate breadcrumb is highlighted + cy.get('[data-attr=onboarding-breadcrumbs] > :nth-child(3) > * span').should('contain', 'Configure') + cy.get('[data-attr=onboarding-breadcrumbs] > :nth-child(3) > * span').should('not.have.css', 'text-muted') - // // Installation should be complete - // cy.get('svg.LemonIcon.text-success').should('exist') - // cy.get('svg.LemonIcon.text-success').parent().should('contain', 'Installation complete') + // Continue to plans + cy.get('[data-attr=onboarding-continue]').click() - // // Continue to configuration step - // cy.get('[data-attr=sdk-continue]').click() + // Click show plans + cy.get('[data-attr=show-plans]').click() - // // Confirm the appropriate breadcrumb is highlighted - // cy.get('[data-attr=onboarding-breadcrumbs] > :nth-child(3) > * span').should('contain', 'Configure') - // cy.get('[data-attr=onboarding-breadcrumbs] > :nth-child(3) > * span').should('not.have.css', 'text-muted') + // Verify pricing table visible + cy.get('.BillingHero').should('be.visible') + cy.get('table.PlanComparison').should('be.visible') - // // Continue to plans - // cy.get('[data-attr=onboarding-continue]').click() + // Continue + cy.get('[data-attr=onboarding-continue]').click() - // // Click show plans - // cy.get('[data-attr=show-plans').click() + // Click back to Install step + cy.get('[data-attr=onboarding-breadcrumbs] > :first-child > * span').click() - // // Verify pricing table visible - // cy.get('.BillingHero').should('be.visible') - // cy.get('table.PlanComparison').should('be.visible') + // Continue through to finish + cy.get('[data-attr=sdk-continue]').click() + cy.get('[data-attr=onboarding-continue]').click() + cy.get('[data-attr=onboarding-continue]').click() + cy.get('[data-attr=onboarding-continue]').click() - // // Continue - // cy.get('[data-attr=onboarding-continue]').click() + // Confirm we're on the insights list page + cy.url().should('contain', 'http://localhost:8080/project/1/insights') - // // Click back to Install step - // cy.get('[data-attr=onboarding-breadcrumbs] > :first-child > * span').click() + cy.visit('/onboarding/product_analytics?step=product_intro') - // // Continue through to finish - // cy.get('[data-attr=sdk-continue]').click() - // cy.get('[data-attr=onboarding-continue]').click() - // cy.get('[data-attr=onboarding-continue]').click() - // cy.get('[data-attr=onboarding-continue]').click() + // Should see both an option to skip onboarding and an option to see the sdk instructions + cy.get('[data-attr=skip-onboarding]').should('be.visible') + cy.get('[data-attr=start-onboarding-sdk]').should('be.visible') - // // Confirm we're on the insights list page - // cy.url().should('eq', 'https://localhost:8080/project/1/insights') - // }) + cy.get('[data-attr=skip-onboarding]').first().click() + cy.url().should('contain', 'http://localhost:8080/project/1/insights') + + cy.visit('/onboarding/product_analytics?step=product_intro') + cy.get('[data-attr=start-onboarding-sdk]').first().click() + cy.url().should('contain', 'http://localhost:8080/project/1/onboarding/product_analytics?step=install') + }) it('Step through SR onboarding', () => { - cy.visit('/replay') + cy.get('[data-attr=menu-item-replay]').click() + cy.get('[data-attr=start-onboarding]').first().click() + // Installation should be complete + cy.get('svg.LemonIcon.text-success').should('exist') + cy.get('svg.LemonIcon.text-success').parent().should('contain', 'Installation complete') + // Continue to configuration step + cy.get('[data-attr=sdk-continue]').click() + // Continue to plans + cy.get('[data-attr=onboarding-continue]').click() + // Go back to intro page + cy.visit('/onboarding/session_replay?step=product_intro') + // Continue through to finish + cy.get('[data-attr=start-onboarding]').first().click() + cy.get('[data-attr=sdk-continue]').click() + cy.get('[data-attr=onboarding-continue]').click() + cy.get('[data-attr=onboarding-continue]').click() + // Confirm we're on the recordings list page + cy.url().should('eq', 'https://localhost:8080/project/1/replay/recent') + + cy.visit('/onboarding/session_replay?step=product_intro') + cy.get('[data-attr=skip-onboarding]').should('be.visible') + cy.get('[data-attr=start-onboarding-sdk]').should('not.exist') }) - // it('Step through FF onboarding', () => {}) + it('Step through FF onboarding', () => { + cy.visit('/onboarding/feature_flags?step=product_intro') + cy.get('[data-attr=menu-item-featureflags]').click() + cy.get('[data-attr=start-onboarding-sdk]').first().click() + cy.get('[data-attr=sdk-continue]').click() - // it('Step through Surveys onboarding', () => {}) + // Confirm the appropriate breadcrumb is highlighted + cy.get('[data-attr=onboarding-breadcrumbs] > :nth-child(5) > * span').should('contain', 'Invite teammates') - // it('Click through product intro pages', () => {}) + cy.visit('/onboarding/feature_flags?step=product_intro') - // it('Product intro pages to onboarding flow', () => {}) + cy.get('[data-attr=skip-onboarding]').should('be.visible') + cy.get('[data-attr=start-onboarding-sdk]').should('be.visible') + + cy.get('[data-attr=skip-onboarding]').click() + cy.url().should('contain', 'feature_flags/new') + }) + + it('Step through Surveys onboarding', () => { + cy.visit('/onboarding/surveys?step=product_intro') + cy.get('[data-attr=skip-onboarding]').should('be.visible') + cy.get('[data-attr=start-onboarding-sdk]').should('not.exist') + cy.get('[data-attr=skip-onboarding]').first().click() + cy.url().should('contain', 'survey_templates') + }) }) diff --git a/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx b/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx index 03eaec32cd9b8..76c5fef0d539f 100644 --- a/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx +++ b/frontend/src/scenes/onboarding/OnboardingProductIntroduction.tsx @@ -95,7 +95,7 @@ const GetStartedButton = ({ product }: { product: BillingProductV2Type }): JSX.E {(!hasSnippetEvents || multiInstallProducts.includes(product.type as ProductKey)) && ( { setTeamPropertiesForProduct(product.type as ProductKey) reportOnboardingProductSelected( diff --git a/frontend/src/scenes/session-recordings/mobile-replay/androidRecordingPromptBannerLogic.ts b/frontend/src/scenes/session-recordings/mobile-replay/androidRecordingPromptBannerLogic.ts new file mode 100644 index 0000000000000..038963b9a277a --- /dev/null +++ b/frontend/src/scenes/session-recordings/mobile-replay/androidRecordingPromptBannerLogic.ts @@ -0,0 +1,99 @@ +import { afterMount, kea, key, path, props, reducers, selectors } from 'kea' +import { loaders } from 'kea-loaders' +import { subscriptions } from 'kea-subscriptions' +import api from 'lib/api' +import posthog from 'posthog-js' + +import { HogQLQuery, NodeKind } from '~/queries/schema' +import { hogql } from '~/queries/utils' + +import type { androidRecordingPromptBannerLogicType } from './androidRecordingPromptBannerLogicType' + +export interface AndroidRecordingPromptBannerLogicProps { + context: 'home' | 'events' | 'replay' +} + +export type AndroidEventCount = { + version: string + count?: number +} + +export const androidRecordingPromptBannerLogic = kea([ + path(['scenes', 'session-recordings', 'SessionRecordings']), + key((props) => props.context), + props({} as AndroidRecordingPromptBannerLogicProps), + loaders(({ values }) => ({ + androidVersions: [ + [] as AndroidEventCount[], + { + loadAndroidLibVersions: async () => { + if (values.androidVersions && values.androidVersions.length > 0) { + // if we know they ever had android events, don't check again + return values.androidVersions + } + + const query: HogQLQuery = { + kind: NodeKind.HogQLQuery, + query: hogql`SELECT properties.$lib_version AS lib_version, + max(timestamp) AS latest_timestamp, + count(lib_version) as count + FROM events + WHERE timestamp >= now() - INTERVAL 30 DAY + AND timestamp <= now() + AND properties.$lib = 'posthog-android' + GROUP BY lib_version + ORDER BY latest_timestamp DESC + limit 10`, + } + + const res = await api.query(query) + + return ( + res.results?.map((x) => ({ + version: x[0], + count: x[2], + })) ?? [] + ) + }, + }, + ], + })), + reducers({ + androidVersions: [ + // as a reducer only so we can persist it + [] as AndroidEventCount[], + { persist: true }, + { + loadAndroidLibVersionsSuccess: (_, { androidVersions }) => { + return androidVersions ?? [] + }, + }, + ], + }), + + selectors({ + shouldPromptUser: [ + (s) => [s.androidVersions], + (androidVersions) => { + return (androidVersions?.length || 0) > 0 + }, + ], + }), + + subscriptions(({ values, props }) => ({ + shouldPromptUser: (value, oldvalue) => { + // not a falsy check since we don't care when oldvalue is undefined + // we don't need multiple copies of this event so try to only emit it when `shouldPromptUser` changes to true + if (value === true && oldvalue === false) { + posthog.capture('visitor has android events', { + androidVersions: values.androidVersions, + scene: props.context, + }) + } + }, + })), + + afterMount(({ actions }) => { + actions.loadAndroidLibVersions() + }), +])