Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mobile replay onboarding #21122

Merged
merged 10 commits into from
Mar 26, 2024
1 change: 1 addition & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export const FEATURE_FLAGS = {
AUDIT_LOGS_ACCESS: 'audit-logs-access', // owner: #team-growth
SUBSCRIBE_FROM_PAYGATE: 'subscribe-from-paygate', // owner: #team-growth
REVERSE_PROXY_ONBOARDING: 'reverse-proxy-onboarding', // owner: @zlwaterfield
SESSION_REPLAY_MOBILE_ONBOARDING: 'session-replay-mobile-onboarding', // owner: #team-replay
} as const
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]

Expand Down
17 changes: 14 additions & 3 deletions frontend/src/scenes/onboarding/Onboarding.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useActions, useValues } from 'kea'
import { SESSION_REPLAY_MINIMUM_DURATION_OPTIONS } from 'lib/constants'
import { FEATURE_FLAGS, SESSION_REPLAY_MINIMUM_DURATION_OPTIONS } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { useEffect, useState } from 'react'
import { AndroidInstructions } from 'scenes/onboarding/sdks/session-replay'
import { SceneExport } from 'scenes/sceneTypes'
import { teamLogic } from 'scenes/teamLogic'
import { userLogic } from 'scenes/userLogic'

import { AvailableFeature, ProductKey } from '~/types'
import { AvailableFeature, ProductKey, SDKKey } from '~/types'

import { OnboardingBillingStep } from './OnboardingBillingStep'
import { OnboardingInviteTeammates } from './OnboardingInviteTeammates'
Expand Down Expand Up @@ -108,6 +110,9 @@ const SessionReplayOnboarding = (): JSX.Element => {
const { hasAvailableFeature } = useValues(userLogic)
const { currentTeam } = useValues(teamLogic)

const { featureFlags } = useValues(featureFlagLogic)
const hasAndroidOnBoarding = !!featureFlags[FEATURE_FLAGS.SESSION_REPLAY_MOBILE_ONBOARDING]

const configOptions: ProductConfigOption[] = [
{
type: 'toggle',
Expand Down Expand Up @@ -139,18 +144,24 @@ const SessionReplayOnboarding = (): JSX.Element => {
})
}

const sdkInstructionMap = SessionReplaySDKInstructions
if (hasAndroidOnBoarding) {
sdkInstructionMap[SDKKey.ANDROID] = AndroidInstructions
}

return (
<OnboardingWrapper>
<SDKs
usersAction="recording sessions"
sdkInstructionMap={SessionReplaySDKInstructions}
sdkInstructionMap={sdkInstructionMap}
subtitle="Choose the framework your frontend is built on, or use our all-purpose JavaScript library. If you already have the snippet installed, you can skip this step!"
stepKey={OnboardingStepKey.INSTALL}
/>
<OnboardingProductConfiguration stepKey={OnboardingStepKey.PRODUCT_CONFIGURATION} options={configOptions} />
</OnboardingWrapper>
)
}

const FeatureFlagsOnboarding = (): JSX.Element => {
return (
<OnboardingWrapper>
Expand Down
18 changes: 12 additions & 6 deletions frontend/src/scenes/onboarding/onboardingLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,9 @@ export const onboardingLogic = kea<onboardingLogicType>([
actionToUrl(({ values }) => ({
setStepKey: ({ stepKey }) => {
if (stepKey) {
return [`/onboarding/${values.productKey}`, { step: stepKey }]
return [`/onboarding/${values.productKey}`, { ...router.values.searchParams, step: stepKey }]
} else {
return [`/onboarding/${values.productKey}`]
return [`/onboarding/${values.productKey}`, router.values.searchParams]
}
},
goToNextStep: () => {
Expand All @@ -327,9 +327,12 @@ export const onboardingLogic = kea<onboardingLogicType>([
)
const nextStep = values.allOnboardingSteps[currentStepIndex + 1]
if (nextStep) {
return [`/onboarding/${values.productKey}`, { step: nextStep.props.stepKey }]
return [
`/onboarding/${values.productKey}`,
{ ...router.values.searchParams, step: nextStep.props.stepKey },
]
} else {
return [`/onboarding/${values.productKey}`]
return [`/onboarding/${values.productKey}`, router.values.searchParams]
}
},
goToPreviousStep: () => {
Expand All @@ -338,9 +341,12 @@ export const onboardingLogic = kea<onboardingLogicType>([
)
const previousStep = values.allOnboardingSteps[currentStepIndex - 1]
if (previousStep) {
return [`/onboarding/${values.productKey}`, { step: previousStep.props.stepKey }]
return [
`/onboarding/${values.productKey}`,
{ ...router.values.searchParams, step: previousStep.props.stepKey },
]
} else {
return [`/onboarding/${values.productKey}`]
return [`/onboarding/${values.productKey}`, router.values.searchParams]
}
},
updateCurrentTeamSuccess(val) {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/scenes/onboarding/sdks/SDKs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export function SDKs({
{sdks?.map((sdk) => (
<React.Fragment key={`sdk-${sdk.key}`}>
<LemonButton
data-attr={`onboarding-sdk-${sdk.key}`}
active={selectedSDK?.key === sdk.key}
onClick={selectedSDK?.key !== sdk.key ? () => setSelectedSDK(sdk) : undefined}
fullWidth
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/scenes/onboarding/sdks/product-analytics/android.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
import { CodeSnippet, Language } from 'lib/components/CodeSnippet'
import { FlaggedFeature } from 'lib/components/FlaggedFeature'
import { FEATURE_FLAGS } from 'lib/constants'
import { LemonTag } from 'lib/lemon-ui/LemonTag'
import { Link } from 'lib/lemon-ui/Link'
import { OnboardingStepKey } from 'scenes/onboarding/onboardingLogic'
import { urls } from 'scenes/urls'

import { SDKKey } from '~/types'

import { SDKInstallAndroidInstructions } from '../sdk-install-instructions'

function AndroidCaptureSnippet(): JSX.Element {
return <CodeSnippet language={Language.Kotlin}>{`PostHog.capture(event = "test-event")`}</CodeSnippet>
}

function AdvertiseAndroidReplay(): JSX.Element {
return (
<div>
<h3 className="mt-8">
Session Replay for Android <LemonTag type="highlight">NEW</LemonTag>
</h3>
<div>
Session replay is now in beta for Android.{' '}
<Link to={urls.onboarding('session_replay', OnboardingStepKey.INSTALL, SDKKey.ANDROID)}>
Learn how to set it up
</Link>
</div>
</div>
)
}

export function ProductAnalyticsAndroidInstructions(): JSX.Element {
return (
<>
<SDKInstallAndroidInstructions />
<h3>Send an Event</h3>
<AndroidCaptureSnippet />
<FlaggedFeature flag={FEATURE_FLAGS.SESSION_REPLAY_MOBILE_ONBOARDING} match={true}>
<AdvertiseAndroidReplay />
</FlaggedFeature>
</>
)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { Link } from '@posthog/lemon-ui'
import { useValues } from 'kea'
import { CodeSnippet, Language } from 'lib/components/CodeSnippet'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { apiHostOrigin } from 'lib/utils/apiHost'
import { teamLogic } from 'scenes/teamLogic'

export interface AndroidSetupProps {
includeReplay?: boolean
}

function AndroidInstallSnippet(): JSX.Element {
return (
<CodeSnippet language={Language.Kotlin}>
Expand All @@ -13,7 +19,7 @@ function AndroidInstallSnippet(): JSX.Element {
)
}

function AndroidSetupSnippet(): JSX.Element {
function AndroidSetupSnippet({ includeReplay }: AndroidSetupProps): JSX.Element {
const { currentTeam } = useValues(teamLogic)

return (
Expand All @@ -33,6 +39,18 @@ function AndroidSetupSnippet(): JSX.Element {
apiKey = POSTHOG_API_KEY,
host = POSTHOG_HOST
)
${
includeReplay
? `
// check https://posthog.com/docs/session-replay/mobile#installation
// for more config and to learn about how we capture sessions on mobile
// and what to expect
config.sessionReplay = true
Copy link
Member

Choose a reason for hiding this comment

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

Should we just point to this link though https://posthog.com/docs/session-replay/mobile#installation?
There are a few nuances such as limitations, the PostHogOkHttpInterceptor bits, etc, so they'd have more context on how it works and less code duplication here.

Copy link
Member Author

Choose a reason for hiding this comment

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

added a code comment with a note... definitely something we can come back to to improve though 👍

// choose whether to mask images or text
config.sessionReplayConfig.maskAllImages = false
config.sessionReplayConfig.maskAllTextInputs = true`
: ''
}

// Setup PostHog with the given Context and Config
PostHogAndroid.setup(this, config)
Expand All @@ -41,13 +59,24 @@ function AndroidSetupSnippet(): JSX.Element {
)
}

export function SDKInstallAndroidInstructions(): JSX.Element {
export function SDKInstallAndroidInstructions(props: AndroidSetupProps): JSX.Element {
return (
<>
{props.includeReplay ? (
<LemonBanner type="info">
🚧 NOTE: <Link to="https://posthog.com/docs/session-replay/mobile">Mobile recording</Link> is
currently in beta. We are keen to gather as much feedback as possible so if you try this out please
let us know. You can send feedback via the{' '}
<Link to="https://us.posthog.com/#panel=support%3Afeedback%3Asession_replay%3Alow">
in-app support panel
</Link>{' '}
or one of our other <Link to="https://posthog.com/docs/support-options">support options</Link>.
</LemonBanner>
) : null}
<h3>Install</h3>
<AndroidInstallSnippet />
<h3>Configure</h3>
<AndroidSetupSnippet />
<AndroidSetupSnippet {...props} />
</>
)
}
29 changes: 20 additions & 9 deletions frontend/src/scenes/onboarding/sdks/sdksLogic.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { actions, afterMount, connect, events, kea, listeners, path, reducers, selectors } from 'kea'
import { loaders } from 'kea-loaders'
import { urlToAction } from 'kea-router'
import api from 'lib/api'
import { LemonSelectOptions } from 'lib/lemon-ui/LemonSelect/LemonSelect'

Expand All @@ -11,7 +12,7 @@ import { onboardingLogic } from '../onboardingLogic'
import { allSDKs } from './allSDKs'
import type { sdksLogicType } from './sdksLogicType'

/*
/*
To add SDK instructions for your product:
1. If needed, add a new ProductKey enum value in ~/types.ts
2. Create a folder in this directory for your product
Expand Down Expand Up @@ -118,14 +119,16 @@ export const sdksLogic = kea<sdksLogicType>([
loadSnippetEvents: async () => {
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 3 DAY
AND timestamp <= now()
AND properties.$lib = 'web'
GROUP BY lib_version
ORDER BY latest_timestamp DESC
limit 10`,
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 3 DAY
AND timestamp <= now()
AND properties.$lib = 'web'
GROUP BY lib_version
ORDER BY latest_timestamp DESC
limit 10`,
}

const res = await api.query(query)
Expand Down Expand Up @@ -188,4 +191,12 @@ export const sdksLogic = kea<sdksLogicType>([
afterMount(({ actions }) => {
actions.loadSnippetEvents()
}),
urlToAction(({ actions }) => ({
'/onboarding/:productKey': (_productKey, { sdk }) => {
const matchedSDK = allSDKs.find((s) => s.key === sdk)
if (matchedSDK) {
actions.setSelectedSDK(matchedSDK)
}
},
})),
])
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ export const SessionReplaySDKInstructions: SDKInstructionsMap = {
[SDKKey.HTML_SNIPPET]: HTMLSnippetInstructions,
[SDKKey.NEXT_JS]: NextJSInstructions,
[SDKKey.REACT]: ReactInstructions,
// added by feature flag in Onboarding.tsx until released
//[SDKKey.ANDROID]: AndroidInstructions,
}
11 changes: 11 additions & 0 deletions frontend/src/scenes/onboarding/sdks/session-replay/android.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { SDKInstallAndroidInstructions } from '../sdk-install-instructions'
import { SessionReplayFinalSteps } from '../shared-snippets'

export function AndroidInstructions(): JSX.Element {
return (
<>
<SDKInstallAndroidInstructions includeReplay={true} />
<SessionReplayFinalSteps />
</>
)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './android'
export * from './html-snippet'
export * from './js-web'
export * from './next-js'
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/scenes/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
PipelineTab,
ProductKey,
ReplayTabs,
SDKKey,
} from '~/types'

import { OnboardingStepKey } from './onboarding/onboardingLogic'
Expand Down Expand Up @@ -175,8 +176,10 @@ export const urls = {
`/verify_email${userUuid ? `/${userUuid}` : ''}${token ? `/${token}` : ''}`,
inviteSignup: (id: string): string => `/signup/${id}`,
products: (): string => '/products',
onboarding: (productKey: string, stepKey?: OnboardingStepKey): string =>
`/onboarding/${productKey}${stepKey ? '?step=' + stepKey : ''}`,
onboarding: (productKey: string, stepKey?: OnboardingStepKey, sdk?: SDKKey): string =>
`/onboarding/${productKey}${stepKey ? '?step=' + stepKey : ''}${
sdk && stepKey ? '&sdk=' + sdk : sdk ? '?sdk=' + sdk : ''
Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome idea, thanks for adding this.

}`,
// Cloud only
organizationBilling: (products?: ProductKey[]): string =>
`/organization/billing${products && products.length ? `?products=${products.join(',')}` : ''}`,
Expand Down
Loading