Skip to content

Commit

Permalink
wip: support test_ api key (#4206)
Browse files Browse the repository at this point in the history
* wip: support test_ api key

* Renamed test_environment property to

* Added Prod/Test switch to determine which environment to show data for

* moved environment toggle behind feature flag

* corrected key name to standard $ names

* Moved hidden filters to PropertyKeyInfo for improved clarity

* fix typing

* proposed UI

* Renamed  to ; Changed  to a string and added environment const types: production, test; Moved  into a hidden filter along with environment types for the frontend; Corrected tests to use environment constants;  is no longer set by default but only when test_ is supplied in the apiKey or  is manually supplied; Moved environment to navigationLogic;

* Added filter for test environments so that when it's enabled all test environments are filtered out

* Remove component if feature flag for test-environment is enabled

* adjust style of tooltip

* adjust UI based on @corywatilo's input

* implemented toggle functionality

* Made Environments an enum for clarity

* Moved environment constants to constants.py for consistency

* Moved Environments into constants for consistency

* Using parameter destructuring for better syntax

* Make sure type is set as a string

* Removed test filters

* Created _clean_token to ensure test_ is removed from all the places

* Bug fixes for adding filter property so that it work properly

* Show test account filter

* ensure token isn't None so that mypy checks pass

* Corrected css selector syntax

* removed quote since it's not needed

* Corrected cypress test by excluding featureFlag usage on shared dashboard scene

* Fixed test

* corrected syntax style issues

Co-authored-by: Paolo D'Amico <[email protected]>
Co-authored-by: Buddy Williams <[email protected]>
  • Loading branch information
3 people authored May 21, 2021
1 parent 2adccd6 commit b15232a
Show file tree
Hide file tree
Showing 14 changed files with 226 additions and 13 deletions.
1 change: 0 additions & 1 deletion cypress/integration/commandPalette.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ describe('Command Palette', () => {
})

it('Shows on Ctrl + K press', () => {
cy.get('[data-attr=insight-trends-tab]').contains('Trends') // Make sure the page is loaded
cy.get('body').type('{ctrl}k')
cy.get('[data-attr=command-palette-input]').should('exist')

Expand Down
2 changes: 1 addition & 1 deletion cypress/integration/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('Dashboard', () => {
.then((link) => {
cy.wait(200)
cy.visit(link)
cy.get('[data-attr="dashboard-item-title"').should('contain', 'Daily Active Users')
cy.get('[data-attr=dashboard-item-title]').should('contain', 'Daily Active Users')
})
})

Expand Down
29 changes: 29 additions & 0 deletions frontend/src/layout/navigation/Navigation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,35 @@
height: 22px;
}
}

.global-environment-switch {
margin-right: $default_spacing;
display: flex;
align-items: center;
label {
font-weight: bold;
color: $success;
padding-right: 4px;
a {
color: $text_muted;
}

svg {
margin-right: 4px;
}

&.test {
color: $warning;
}
}
.ant-switch:not(.ant-switch-checked) {
background-color: $warning;
}

.ant-switch.ant-switch-checked {
background-color: $success;
}
}
}

.navigation-spacer {
Expand Down
40 changes: 37 additions & 3 deletions frontend/src/layout/navigation/TopNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { userLogic } from 'scenes/userLogic'
import { Badge } from 'lib/components/Badge'
import { ChangelogModal } from '~/layout/ChangelogModal'
import { router } from 'kea-router'
import { Button, Card, Dropdown, Tooltip } from 'antd'
import { Button, Card, Dropdown, Switch, Tooltip } from 'antd'
import {
ProjectOutlined,
DownOutlined,
Expand All @@ -32,6 +32,8 @@ import { UserType } from '~/types'
import { CreateInviteModalWithButton } from 'scenes/organization/Settings/CreateInviteModal'
import { preflightLogic } from 'scenes/PreflightCheck/logic'
import { billingLogic } from 'scenes/billing/billingLogic'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { Environments } from 'lib/constants'
import md5 from 'md5'

export interface ProfilePictureProps {
Expand Down Expand Up @@ -74,10 +76,17 @@ export function WhoAmI({ user }: { user: UserType }): JSX.Element {
}

export function TopNavigation(): JSX.Element {
const { setMenuCollapsed, setChangelogModalOpen, setInviteMembersModalOpen } = useActions(navigationLogic)
const { menuCollapsed, systemStatus, updateAvailable, changelogModalOpen, inviteMembersModalOpen } = useValues(
const { setMenuCollapsed, setChangelogModalOpen, setInviteMembersModalOpen, setFilteredEnvironment } = useActions(
navigationLogic
)
const {
menuCollapsed,
systemStatus,
updateAvailable,
changelogModalOpen,
inviteMembersModalOpen,
filteredEnvironment,
} = useValues(navigationLogic)
const { user } = useValues(userLogic)
const { preflight } = useValues(preflightLogic)
const { billing } = useValues(billingLogic)
Expand All @@ -88,6 +97,7 @@ export function TopNavigation(): JSX.Element {
const { showPalette } = useActions(commandPaletteLogic)
const [projectModalShown, setProjectModalShown] = useState(false) // TODO: Move to Kea (using useState for backwards-compatibility with TopSelectors.tsx)
const [organizationModalShown, setOrganizationModalShown] = useState(false) // TODO: Same as above
const { featureFlags } = useValues(featureFlagLogic)

const whoAmIDropdown = (
<div className="navigation-top-dropdown whoami-dropdown">
Expand Down Expand Up @@ -309,6 +319,30 @@ export function TopNavigation(): JSX.Element {
</div>
{user && (
<div>
{featureFlags['test-environment-3149'] && (
<div className="global-environment-switch">
<label
htmlFor="global-environment-switch"
className={filteredEnvironment === Environments.TEST ? 'test' : ''}
>
<Tooltip title="Toggle to view only test or production data everywhere. Click to learn more.">
<a href="https://posthog.com/docs" target="_blank" rel="noopener">
<InfoCircleOutlined />
</a>
</Tooltip>
{filteredEnvironment === Environments.PRODUCTION ? 'Production' : 'Test'}
</label>
<Switch
// @ts-expect-error - below works even if it's not defined as a prop
id="global-environment-switch"
checked={filteredEnvironment === Environments.PRODUCTION}
defaultChecked={filteredEnvironment === Environments.PRODUCTION}
onChange={(val) =>
setFilteredEnvironment(val ? Environments.PRODUCTION : Environments.TEST)
}
/>
</div>
)}
<Dropdown overlay={whoAmIDropdown} trigger={['click']}>
<div>
<WhoAmI user={user} />
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/layout/navigation/navigationLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { organizationLogic } from 'scenes/organizationLogic'
import dayjs from 'dayjs'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { preflightLogic } from 'scenes/PreflightCheck/logic'
import { Environments, ENVIRONMENT_LOCAL_STORAGE_KEY } from 'lib/constants'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'

type WarningType =
| 'welcome'
Expand All @@ -27,6 +29,7 @@ export const navigationLogic = kea<navigationLogicType<UserType, SystemStatus, W
setPinnedDashboardsVisible: (visible: boolean) => ({ visible }),
setInviteMembersModalOpen: (isOpen: boolean) => ({ isOpen }),
setHotkeyNavigationEngaged: (hotkeyNavigationEngaged: boolean) => ({ hotkeyNavigationEngaged }),
setFilteredEnvironment: (environment: string, pageLoad: boolean = false) => ({ environment, pageLoad }),
},
reducers: {
menuCollapsed: [
Expand Down Expand Up @@ -65,6 +68,12 @@ export const navigationLogic = kea<navigationLogicType<UserType, SystemStatus, W
setHotkeyNavigationEngaged: (_, { hotkeyNavigationEngaged }) => hotkeyNavigationEngaged,
},
],
filteredEnvironment: [
Environments.PRODUCTION.toString(),
{
setFilteredEnvironment: (_, { environment }) => environment,
},
],
},
selectors: {
systemStatus: [
Expand Down Expand Up @@ -162,9 +171,27 @@ export const navigationLogic = kea<navigationLogicType<UserType, SystemStatus, W
actions.setHotkeyNavigationEngaged(false)
}
},
setFilteredEnvironment: ({ pageLoad, environment }) => {
const localStorageValue = window.localStorage.getItem(ENVIRONMENT_LOCAL_STORAGE_KEY)
const isLocalStorageValueEmpty = localStorageValue === null
const shouldWriteToLocalStorage = (pageLoad === true && isLocalStorageValueEmpty) || pageLoad === false
if (shouldWriteToLocalStorage) {
window.localStorage.setItem(ENVIRONMENT_LOCAL_STORAGE_KEY, environment)
}
const shouldReload = pageLoad === false && localStorageValue !== environment
if (shouldReload) {
location.reload()
}
},
}),
events: ({ actions }) => ({
afterMount: () => {
const notSharedDashboard = location.pathname.indexOf('shared_dashboard') > -1 ? false : true
if (notSharedDashboard && featureFlagLogic.values.featureFlags['test-environment-3149']) {
const localStorageValue =
window.localStorage.getItem(ENVIRONMENT_LOCAL_STORAGE_KEY) || Environments.PRODUCTION
actions.setFilteredEnvironment(localStorageValue, true)
}
actions.loadLatestVersion()
},
}),
Expand Down
56 changes: 53 additions & 3 deletions frontend/src/lib/api.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { fromParamsGivenUrl, toParams } from 'lib/utils'
import { Environments, ENVIRONMENT_LOCAL_STORAGE_KEY } from 'lib/constants'

export function getCookie(name) {
var cookieValue = null
if (document.cookie && document.cookie !== '') {
Expand All @@ -24,9 +27,8 @@ async function getJSONOrThrow(response) {

class Api {
async get(url) {
if (url.indexOf('http') !== 0) {
url = '/' + url + (url.indexOf('?') === -1 && url[url.length - 1] !== '/' ? '/' : '')
}
// TODO: how to put behind a feature flag
url = maybeAddEnvironmentProperty(url)

let response
try {
Expand Down Expand Up @@ -103,5 +105,53 @@ class Api {
return response
}
}

function isWhitelisted(url) {
const WHITELIST = ['api']

for (let i = 0; i < WHITELIST.length; i++) {
const urlWithSlash = '/' + url
const startsWith = url.indexOf(WHITELIST[i]) === 0 || urlWithSlash.indexOf(WHITELIST[i]) === 0
if (startsWith) {
return true
}
}

return false
}

function maybeAddEnvironmentProperty(url) {
const localStorageEnvironmentValue = window.localStorage.getItem(ENVIRONMENT_LOCAL_STORAGE_KEY)
const isWhitelistedUrl = isWhitelisted(url)
const shouldAddEnvironmentValue = localStorageEnvironmentValue && isWhitelistedUrl

if (shouldAddEnvironmentValue) {
let urlObject = url.indexOf('http') === 0 ? new URL(url) : new URL(url, window.location.origin)

let params = fromParamsGivenUrl(urlObject.search)

const environmentProperty =
localStorageEnvironmentValue === Environments.PRODUCTION
? { key: '$environment', operator: 'is_not', value: ['test'] }
: { key: '$environment', operator: 'exact', value: ['test'] }

if (params.properties) {
let parsedProperties = JSON.parse(params.properties)
parsedProperties = Array.isArray(parsedProperties)
? [...parsedProperties, environmentProperty]
: [parsedProperties, environmentProperty]
params.properties = JSON.stringify(parsedProperties)
} else {
params.properties = JSON.stringify([environmentProperty])
}

return url.indexOf('http') === 0
? urlObject.origin + urlObject.pathname + '?' + toParams(params)
: urlObject.pathname + '?' + toParams(params)
} else if (url.indexOf('http') !== 0) {
return '/' + url + (url.indexOf('?') === -1 && url[url.length - 1] !== '/' ? '/' : '')
}
}

let api = new Api()
export default api
6 changes: 6 additions & 0 deletions frontend/src/lib/components/PropertyKeyInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,12 @@ export const keyMapping: KeyMappingInterface = {
examples: ['16ff262c4301e5-0aa346c03894bc-39667c0e-1aeaa0-16ff262c431767'],
hide: true,
},
$environment: {
label: 'Environment',
description: 'Environment used to filter results on all queries when enabled.',
examples: ['test', 'production'],
hide: true,
},

// GeoIP
$geoip_city_name: {
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,10 @@ export const FEATURE_FLAGS: Record<string, string> = {
QUERY_UX_V2: '4050-query-ui-optB',
EVENT_COLUMN_CONFIG: '4141-event-columns',
}

export const ENVIRONMENT_LOCAL_STORAGE_KEY = '$environment'

export enum Environments {
PRODUCTION = 'production',
TEST = 'test',
}
10 changes: 7 additions & 3 deletions frontend/src/lib/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ export function toParams(obj: Record<string, any>): string {
.join('&')
}

export function fromParams(): Record<string, any> {
return !window.location.search
export function fromParamsGivenUrl(url: string): Record<string, any> {
return !url
? {}
: window.location.search
: url
.slice(1)
.split('&')
.reduce((paramsObject, paramString) => {
Expand All @@ -56,6 +56,10 @@ export function fromParams(): Record<string, any> {
}, {} as Record<string, any>)
}

export function fromParams(): Record<string, any> {
return fromParamsGivenUrl(window.location.search)
}

export function percentage(division: number): string {
return division
? division.toLocaleString(undefined, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React from 'react'
import { FilterType } from '~/types'
import { SettingOutlined } from '@ant-design/icons'
import { teamLogic } from 'scenes/teamLogic'
//import { featureFlagLogic } from 'lib/logic/featureFlagLogic'

export function TestAccountFilter({
filters,
Expand All @@ -15,7 +16,9 @@ export function TestAccountFilter({
}): JSX.Element | null {
const { currentTeam } = useValues(teamLogic)
const hasFilters = (currentTeam?.test_account_filters || []).length > 0
//const { featureFlags } = useValues(featureFlagLogic)

//return featureFlags['test-environment-3149'] ? null : (
return (
<Tooltip
title={
Expand Down
15 changes: 15 additions & 0 deletions posthog/api/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from statshog.defaults.django import statsd

from posthog.celery import app as celery_app
from posthog.constants import ENVIRONMENT_TEST
from posthog.ee import is_clickhouse_enabled
from posthog.exceptions import RequestParsingError, generate_exception_response
from posthog.helpers.session_recording import preprocess_session_recording_events
Expand Down Expand Up @@ -97,6 +98,13 @@ def _get_token(data, request) -> Optional[str]:
return None


# Support test_[apiKey] for users with multiple environments
def _clean_token(token):
is_test_environment = token.startswith("test_")
token = token[5:] if is_test_environment else token
return token, is_test_environment


def _get_project_id(data, request) -> Optional[int]:
if request.GET.get("project_id"):
return int(request.POST["project_id"])
Expand Down Expand Up @@ -166,6 +174,9 @@ def get_event(request):
),
)

token, is_test_environment = _clean_token(token)
assert token is not None

team = Team.objects.get_team_from_token(token)

if team is None:
Expand Down Expand Up @@ -259,6 +270,10 @@ def get_event(request):
if not event.get("properties"):
event["properties"] = {}

# Support test_[apiKey] for users with multiple environments
if event["properties"].get("$environment") is None and is_test_environment:
event["properties"]["$environment"] = ENVIRONMENT_TEST

_ensure_web_feature_flags_in_properties(event, team, distinct_id)

statsd.incr("posthog_cloud_plugin_server_ingestion")
Expand Down
3 changes: 2 additions & 1 deletion posthog/api/decide.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from posthog.models.feature_flag import get_active_feature_flags
from posthog.utils import cors_response, load_data_from_request

from .capture import _get_project_id, _get_token
from .capture import _clean_token, _get_project_id, _get_token


def on_permitted_domain(team: Team, request: HttpRequest) -> bool:
Expand Down Expand Up @@ -89,6 +89,7 @@ def get_decide(request: HttpRequest):
generate_exception_response("decide", f"Malformed request data: {error}", code="malformed_data"),
)
token = _get_token(data, request)
token, is_test_environment = _clean_token(token)
team = Team.objects.get_team_from_token(token)
if team is None and token:
project_id = _get_project_id(data, request)
Expand Down
Loading

0 comments on commit b15232a

Please sign in to comment.