diff --git a/.eslintrc.js b/.eslintrc.js index 9d54f523057d0..9d6792f0fd652 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -86,7 +86,7 @@ module.exports = { }, ], 'react/forbid-dom-props': [ - 1, + 'warn', { forbid: [ { @@ -98,7 +98,7 @@ module.exports = { }, ], 'posthog/warn-elements': [ - 1, + 'warn', { forbid: [ { @@ -146,7 +146,7 @@ module.exports = { }, ], 'react/forbid-elements': [ - 2, + 'error', { forbid: [ { @@ -200,9 +200,9 @@ module.exports = { ], }, ], - 'no-constant-condition': 0, - 'no-prototype-builtins': 0, - 'no-irregular-whitespace': 0, + 'no-constant-condition': 'off', + 'no-prototype-builtins': 'off', + 'no-irregular-whitespace': 'off', }, overrides: [ { diff --git a/bin/docker-server-unit b/bin/docker-server-unit index 1eda8374759a5..f1caab6a71174 100755 --- a/bin/docker-server-unit +++ b/bin/docker-server-unit @@ -10,4 +10,6 @@ trap 'rm -rf "$PROMETHEUS_MULTIPROC_DIR"' EXIT export PROMETHEUS_METRICS_EXPORT_PORT=8001 export STATSD_PORT=${STATSD_PORT:-8125} -exec /usr/local/bin/docker-entrypoint.sh unitd --no-daemon +# We need to run as --user root so that nginx unit can proxy the control socket for stats +# However each application is run as "nobody" +exec /usr/local/bin/docker-entrypoint.sh unitd --no-daemon --user root diff --git a/bin/unit_metrics.py b/bin/unit_metrics.py new file mode 100644 index 0000000000000..227139cf10b09 --- /dev/null +++ b/bin/unit_metrics.py @@ -0,0 +1,78 @@ +import http.client +import json +from prometheus_client import CollectorRegistry, Gauge, multiprocess, generate_latest + +UNIT_CONNECTIONS_ACCEPTED_TOTAL = Gauge( + "unit_connections_accepted_total", + "", + multiprocess_mode="livesum", +) +UNIT_CONNECTIONS_ACTIVE = Gauge( + "unit_connections_active", + "", + multiprocess_mode="livesum", +) +UNIT_CONNECTIONS_CLOSED = Gauge( + "unit_connections_closed", + "", + multiprocess_mode="livesum", +) +UNIT_CONNECTIONS_IDLE = Gauge( + "unit_connections_idle", + "", + multiprocess_mode="livesum", +) +UNIT_CONNECTIONS_TOTAL = Gauge( + "unit_requests_total", + "", + multiprocess_mode="livesum", +) +UNIT_PROCESSES_RUNNING_GAUGE = Gauge( + "unit_application_processes_running", "", multiprocess_mode="livesum", labelnames=["application"] +) +UNIT_PROCESSES_STARTING_GAUGE = Gauge( + "unit_application_processes_starting", "", multiprocess_mode="livesum", labelnames=["application"] +) +UNIT_PROCESSES_IDLE_GAUGE = Gauge( + "unit_application_processes_idle", "", multiprocess_mode="livesum", labelnames=["application"] +) +UNIT_REQUESTS_ACTIVE_GAUGE = Gauge( + "unit_application_requests_active", "", multiprocess_mode="livesum", labelnames=["application"] +) + + +def application(environ, start_response): + connection = http.client.HTTPConnection("localhost:8081") + connection.request("GET", "/status") + response = connection.getresponse() + + statj = json.loads(response.read()) + connection.close() + + UNIT_CONNECTIONS_ACCEPTED_TOTAL.set(statj["connections"]["accepted"]) + UNIT_CONNECTIONS_ACTIVE.set(statj["connections"]["active"]) + UNIT_CONNECTIONS_IDLE.set(statj["connections"]["idle"]) + UNIT_CONNECTIONS_CLOSED.set(statj["connections"]["closed"]) + UNIT_CONNECTIONS_TOTAL.set(statj["requests"]["total"]) + + for application in statj["applications"].keys(): + UNIT_PROCESSES_RUNNING_GAUGE.labels(application=application).set( + statj["applications"][application]["processes"]["running"] + ) + UNIT_PROCESSES_STARTING_GAUGE.labels(application=application).set( + statj["applications"][application]["processes"]["starting"] + ) + UNIT_PROCESSES_IDLE_GAUGE.labels(application=application).set( + statj["applications"][application]["processes"]["idle"] + ) + UNIT_REQUESTS_ACTIVE_GAUGE.labels(application=application).set( + statj["applications"][application]["requests"]["active"] + ) + + start_response("200 OK", [("Content-Type", "text/plain")]) + # Create the prometheus multi-process metric registry here + # This will aggregate metrics we send from the Django app + # We prepend our unit metrics here. + registry = CollectorRegistry() + multiprocess.MultiProcessCollector(registry) + yield generate_latest(registry) diff --git a/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png b/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png index 363659a9b5866..d6139bbdf9627 100644 Binary files a/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png and b/frontend/__snapshots__/filters-propertyfilters--with-no-close-button.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png index 20983c9ce3d2c..b41b5c6455c6d 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png index 0aead0fac3794..500762ca60561 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png index 772ae64573f6a..77cc5d1496a9c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2.png index a931bed1128b3..726f754c99356 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2.png differ diff --git a/frontend/src/lib/components/IntervalFilter/intervalFilterLogic.ts b/frontend/src/lib/components/IntervalFilter/intervalFilterLogic.ts new file mode 100644 index 0000000000000..d2136d8d8a682 --- /dev/null +++ b/frontend/src/lib/components/IntervalFilter/intervalFilterLogic.ts @@ -0,0 +1,144 @@ +import { kea, props, key, path, connect, actions, reducers, listeners } from 'kea' +import { objectsEqual, dateMapping } from 'lib/utils' +import type { intervalFilterLogicType } from './intervalFilterLogicType' +import { IntervalKeyType, Intervals, intervals } from 'lib/components/IntervalFilter/intervals' +import { BaseMathType, InsightLogicProps, IntervalType } from '~/types' +import { keyForInsightLogicProps } from 'scenes/insights/sharedUtils' +import { dayjs } from 'lib/dayjs' +import { InsightQueryNode, TrendsQuery } from '~/queries/schema' +import { lemonToast } from 'lib/lemon-ui/lemonToast' +import { BASE_MATH_DEFINITIONS } from 'scenes/trends/mathsLogic' +import { insightVizDataLogic } from 'scenes/insights/insightVizDataLogic' + +export const intervalFilterLogic = kea([ + props({} as InsightLogicProps), + key(keyForInsightLogicProps('new')), + path((key) => ['lib', 'components', 'IntervalFilter', 'intervalFilterLogic', key]), + connect((props: InsightLogicProps) => ({ + actions: [insightVizDataLogic(props), ['updateQuerySource']], + values: [insightVizDataLogic(props), ['interval', 'querySource']], + })), + actions(() => ({ + setInterval: (interval: IntervalKeyType) => ({ interval }), + setEnabledIntervals: (enabledIntervals: Intervals) => ({ enabledIntervals }), + })), + reducers(() => ({ + enabledIntervals: [ + { ...intervals } as Intervals, + { + setEnabledIntervals: (_, { enabledIntervals }) => enabledIntervals, + }, + ], + })), + listeners(({ values, actions, selectors }) => ({ + setInterval: ({ interval }) => { + if (values.interval !== interval) { + actions.updateQuerySource({ interval } as Partial) + } + }, + updateQuerySource: ({ querySource }, _, __, previousState) => { + const { date_from, date_to } = querySource.dateRange || {} + const previousDateRange = selectors.querySource(previousState)?.dateRange || {} + + let activeUsersMath: BaseMathType.WeeklyActiveUsers | BaseMathType.MonthlyActiveUsers | null = null + + // We disallow grouping by certain intervals for weekly active users and monthly active users views + // e.g. WAUs grouped by month. Here, look for the first event/action running WAUs/MAUs math and + // pass that down to the interval filter to determine what groupings are allowed. + for (const series of (values.querySource as TrendsQuery)?.series || []) { + if (series.math === BaseMathType.WeeklyActiveUsers) { + activeUsersMath = BaseMathType.WeeklyActiveUsers + break + } + + if (series.math === BaseMathType.MonthlyActiveUsers) { + activeUsersMath = BaseMathType.MonthlyActiveUsers + break + } + } + + const enabledIntervals: Intervals = { ...intervals } + + if (activeUsersMath) { + // Disallow grouping by hour for WAUs/MAUs as it's an expensive query that produces a view that's not useful for users + enabledIntervals.hour = { + ...enabledIntervals.hour, + disabledReason: + 'Grouping by hour is not supported on insights with weekly or monthly active users series.', + } + + // Disallow grouping by month for WAUs as the resulting view is misleading to users + if (activeUsersMath === BaseMathType.WeeklyActiveUsers) { + enabledIntervals.month = { + ...enabledIntervals.month, + disabledReason: + 'Grouping by month is not supported on insights with weekly active users series.', + } + } + } + + actions.setEnabledIntervals(enabledIntervals) + + // If the user just flipped an event action to use WAUs/MAUs math and their + // current interval is unsupported by the math type, switch their interval + // to an appropriate allowed interval and inform them of the change via a toast + if ( + activeUsersMath && + (values.querySource as TrendsQuery)?.interval && + enabledIntervals[(values.querySource as TrendsQuery).interval as IntervalType].disabledReason + ) { + if (values.interval === 'hour') { + lemonToast.info( + `Switched to grouping by day, because "${BASE_MATH_DEFINITIONS[activeUsersMath].name}" does not support grouping by ${values.interval}.` + ) + actions.updateQuerySource({ interval: 'day' } as Partial) + } else { + lemonToast.info( + `Switched to grouping by week, because "${BASE_MATH_DEFINITIONS[activeUsersMath].name}" does not support grouping by ${values.interval}.` + ) + actions.updateQuerySource({ interval: 'week' } as Partial) + } + return + } + + if ( + !date_from || + (objectsEqual(date_from, previousDateRange.date_from) && + objectsEqual(date_to, previousDateRange.date_to)) + ) { + return + } + + // automatically set an interval for fixed date ranges + if ( + date_from && + date_to && + dayjs(querySource.dateRange?.date_from).isValid() && + dayjs(querySource.dateRange?.date_to).isValid() + ) { + if (dayjs(date_to).diff(dayjs(date_from), 'day') <= 3) { + actions.updateQuerySource({ interval: 'hour' } as Partial) + } else if (dayjs(date_to).diff(dayjs(date_from), 'month') <= 3) { + actions.updateQuerySource({ interval: 'day' } as Partial) + } else { + actions.updateQuerySource({ interval: 'month' } as Partial) + } + return + } + // get a defaultInterval for dateOptions that have a default value + let interval: IntervalType = 'day' + for (const { key, values, defaultInterval } of dateMapping) { + if ( + values[0] === date_from && + values[1] === (date_to || undefined) && + key !== 'Custom' && + defaultInterval + ) { + interval = defaultInterval + break + } + } + actions.updateQuerySource({ interval } as Partial) + }, + })), +]) diff --git a/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx b/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx index d981c95aad5a7..d14d233f90f3b 100644 --- a/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx +++ b/frontend/src/lib/components/PersonPropertySelect/PersonPropertySelect.tsx @@ -36,6 +36,7 @@ const SortableProperty = ({ className={clsx(sortable ? 'cursor-move' : 'cursor-auto')} {...attributes} {...listeners} + // eslint-disable-next-line react/forbid-dom-props style={{ transform: CSS.Translate.toString(transform), transition, diff --git a/frontend/src/lib/components/PropertyFilters/PropertyFilters.stories.tsx b/frontend/src/lib/components/PropertyFilters/PropertyFilters.stories.tsx index fb925b583c669..484b294b29b47 100644 --- a/frontend/src/lib/components/PropertyFilters/PropertyFilters.stories.tsx +++ b/frontend/src/lib/components/PropertyFilters/PropertyFilters.stories.tsx @@ -38,7 +38,6 @@ export function ComparingPropertyFilters(): JSX.Element { propertyFilters={[...propertyFilters]} onChange={() => {}} pageKey={'pageKey'} - style={{ marginBottom: 0 }} showNestedArrow eventNames={[]} /> @@ -48,7 +47,6 @@ export function ComparingPropertyFilters(): JSX.Element { propertyFilters={[...propertyFilters]} onChange={() => {}} pageKey={'pageKey'} - style={{ marginBottom: 0 }} eventNames={[]} disablePopover={true} /> diff --git a/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx b/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx index 18415dba6c362..dc9506368a0cd 100644 --- a/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx +++ b/frontend/src/lib/components/PropertyFilters/PropertyFilters.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties, useEffect } from 'react' +import React, { useEffect } from 'react' import { useValues, BindLogic, useActions } from 'kea' import { propertyFilterLogic } from './propertyFilterLogic' import { FilterRow } from './components/FilterRow' @@ -15,7 +15,6 @@ interface PropertyFiltersProps { pageKey: string showConditionBadge?: boolean disablePopover?: boolean - style?: CSSProperties taxonomicGroupTypes?: TaxonomicFilterGroupType[] hogQLTable?: string showNestedArrow?: boolean @@ -39,7 +38,6 @@ export function PropertyFilters({ disablePopover = false, // use bare PropertyFilter without popover taxonomicGroupTypes, hogQLTable, - style = {}, showNestedArrow = false, eventNames = [], orFiltering = false, @@ -62,7 +60,7 @@ export function PropertyFilters({ }, [propertyFilters]) return ( -
+
{showNestedArrow && !disablePopover &&
{<>↳}
}
diff --git a/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx b/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx index 838569a81c8ab..817677a1f7f8a 100644 --- a/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/OperatorValueSelect.tsx @@ -126,7 +126,7 @@ export function OperatorValueSelect({ />
{!isOperatorFlag(currentOperator || PropertyOperator.Exact) && type && propkey && ( -
+
= ({ filters, style }: Props) => { +const PropertyFiltersDisplay = ({ filters }: { filters: AnyPropertyFilter[] }): JSX.Element => { return ( -
+
{filters && filters.map((item) => { return diff --git a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx index f0d26edd7c1a4..30d7c835ccf9f 100644 --- a/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx +++ b/frontend/src/lib/components/PropertyGroupFilters/PropertyGroupFilters.tsx @@ -113,7 +113,6 @@ export function PropertyGroupFilters({ ? (group.values as AnyPropertyFilter[]) : null } - style={{ marginBottom: 0 }} onChange={(properties) => { setPropertyFilters(properties, propertyGroupIndex) }} diff --git a/frontend/src/lib/components/SeriesGlyph.tsx b/frontend/src/lib/components/SeriesGlyph.tsx index a34d6337951e7..156ebcf5f367b 100644 --- a/frontend/src/lib/components/SeriesGlyph.tsx +++ b/frontend/src/lib/components/SeriesGlyph.tsx @@ -10,6 +10,7 @@ interface SeriesGlyphProps { export function SeriesGlyph({ className, style, children, variant }: SeriesGlyphProps): JSX.Element { return ( + // eslint-disable-next-line react/forbid-dom-props
{children}
diff --git a/frontend/src/lib/components/Table/Table.tsx b/frontend/src/lib/components/Table/Table.tsx index 6fc3d67005941..bc8a41e524eec 100644 --- a/frontend/src/lib/components/Table/Table.tsx +++ b/frontend/src/lib/components/Table/Table.tsx @@ -13,7 +13,7 @@ export function createdAtColumn = Record +
) @@ -33,6 +33,7 @@ export function createdByColumn = Record )} + {/* eslint-disable-next-line react/forbid-dom-props */}
{item.created_by ? item.created_by.first_name || item.created_by.email : '-'}
diff --git a/frontend/src/lib/components/Table/utils.tsx b/frontend/src/lib/components/Table/utils.tsx index 877dbd3ec43b0..8bd296810ae6f 100644 --- a/frontend/src/lib/components/Table/utils.tsx +++ b/frontend/src/lib/components/Table/utils.tsx @@ -2,7 +2,7 @@ import { useWindowSize } from 'lib/hooks/useWindowSize' import { getBreakpoint } from 'lib/utils/responsiveUtils' export function normalizeColumnTitle(title: string | JSX.Element): JSX.Element { - return {title} + return {title} } // Returns a boolean indicating whether table should be scrolling or not given a specific diff --git a/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx b/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx index 5496ec2b95e6e..777e11cf0c6ff 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx @@ -107,7 +107,7 @@ export function InfiniteSelectResults({
{taxonomicGroupTypes.map((groupType) => { return ( -
+
-

+

I am a custom footer!
This might be a good time to tell you about our premium features...

diff --git a/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx b/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx index c97db6270d428..4e855d4ba00b7 100644 --- a/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx +++ b/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx' import { useValues } from 'kea' import md5 from 'md5' -import { CSSProperties, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { userLogic } from 'scenes/userLogic' import { IconRobot } from '../icons' import { Lettermark, LettermarkColor } from '../Lettermark/Lettermark' @@ -13,7 +13,6 @@ export interface ProfilePictureProps { email?: string size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' showName?: boolean - style?: CSSProperties className?: string title?: string index?: number @@ -25,7 +24,6 @@ export function ProfilePicture({ email, size = 'lg', showName, - style, className, index, title, @@ -64,7 +62,6 @@ export function ProfilePicture({ src={gravatarUrl} title={title || `This is the Gravatar for ${combinedNameAndEmail}`} alt="" - style={style} /> ) } else { @@ -72,7 +69,7 @@ export function ProfilePicture({ type === 'bot' ? ( ) : ( - + This variable will be set to the distinct ID if you've called{' '} -
posthog.identify('distinct id')
. If the user is anonymous, - it'll be empty. +
posthog.identify('distinct id')
. If the user is anonymous, it'll be + empty.
), }, diff --git a/frontend/src/queries/nodes/EventsNode/EventPropertyFilters.tsx b/frontend/src/queries/nodes/EventsNode/EventPropertyFilters.tsx index f3788bc9a87cb..d7dc068310111 100644 --- a/frontend/src/queries/nodes/EventsNode/EventPropertyFilters.tsx +++ b/frontend/src/queries/nodes/EventsNode/EventPropertyFilters.tsx @@ -36,7 +36,6 @@ export function EventPropertyFilters({ query, setQuery }: EventPropertyFiltersPr } }} pageKey={`EventPropertyFilters.${id}`} - style={{ marginBottom: 0, marginTop: 0 }} eventNames={eventNames} /> ) : ( diff --git a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.tsx b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.tsx index a21ea23176bf9..ba0abd3be7b46 100644 --- a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.tsx +++ b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/PropertyGroupFilters.tsx @@ -101,7 +101,6 @@ export function PropertyGroupFilters({ ? (group.values as AnyPropertyFilter[]) : null } - style={{ marginBottom: 0 }} onChange={(properties) => { setPropertyFilters(properties, propertyGroupIndex) }} diff --git a/frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx b/frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx index e99c2c68b7ffd..f8ddaa48b44b1 100644 --- a/frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx +++ b/frontend/src/queries/nodes/PersonsNode/PersonPropertyFilters.tsx @@ -33,7 +33,6 @@ export function PersonPropertyFilters({ query, setQuery }: PersonPropertyFilters : [TaxonomicFilterGroupType.PersonProperties] } hogQLTable="persons" - style={{ marginBottom: 0, marginTop: 0 }} /> ) : (
Error: property groups are not supported.
diff --git a/frontend/src/scenes/PreflightCheck/preflightLogic.tsx b/frontend/src/scenes/PreflightCheck/preflightLogic.tsx index 83dfe166591f2..ae958b00c996b 100644 --- a/frontend/src/scenes/PreflightCheck/preflightLogic.tsx +++ b/frontend/src/scenes/PreflightCheck/preflightLogic.tsx @@ -259,6 +259,12 @@ export const preflightLogic = kea([ return preflight?.cloud || preflight?.is_debug }, ], + isDev: [ + (s) => [s.preflight], + (preflight): boolean | undefined => { + return preflight?.is_debug + }, + ], }), listeners(({ values, actions }) => ({ handlePreflightFinished: () => { diff --git a/frontend/src/scenes/ResourcePermissionModal.tsx b/frontend/src/scenes/ResourcePermissionModal.tsx index de8706a13e266..7e4f9a1ca9a9d 100644 --- a/frontend/src/scenes/ResourcePermissionModal.tsx +++ b/frontend/src/scenes/ResourcePermissionModal.tsx @@ -181,12 +181,7 @@ export function ResourcePermission({ <>
Roles
{roles.length > 0 ? ( -
+
{roles.map((role) => { return ( {type} } diff --git a/frontend/src/scenes/billing/BillingGauge.tsx b/frontend/src/scenes/billing/BillingGauge.tsx index cce1c0b9663a4..27e41faf153d5 100644 --- a/frontend/src/scenes/billing/BillingGauge.tsx +++ b/frontend/src/scenes/billing/BillingGauge.tsx @@ -14,12 +14,8 @@ type BillingGaugeItemProps = { const BillingGaugeItem = ({ width, className, tooltip, top, value }: BillingGaugeItemProps): JSX.Element => { return ( -
+ // eslint-disable-next-line react/forbid-dom-props +
setInnerGroupType(value, groupIndex)} value={group.type} /> -
+
} status="primary-alt" diff --git a/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx b/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx index 83f133b41035b..4f7209a7d5583 100644 --- a/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx +++ b/frontend/src/scenes/cohorts/CohortFilters/CohortCriteriaRowBuilder.tsx @@ -121,7 +121,7 @@ export function CohortCriteriaRowBuilder({ /> )}
-
+
diff --git a/frontend/src/scenes/dashboard/DashboardCollaborators.tsx b/frontend/src/scenes/dashboard/DashboardCollaborators.tsx index 2c91d4ff3000c..4f13b597f8fee 100644 --- a/frontend/src/scenes/dashboard/DashboardCollaborators.tsx +++ b/frontend/src/scenes/dashboard/DashboardCollaborators.tsx @@ -88,12 +88,7 @@ export function DashboardCollaboration({ dashboardId }: { dashboardId: Dashboard
)}
Project members with access
-
+
{allCollaborators.map((collaborator) => ( { }) } > -
+
@@ -36,7 +36,7 @@ export const NoDashboards = (): JSX.Element => { }) } > -
+
diff --git a/frontend/src/scenes/events/Owner.tsx b/frontend/src/scenes/events/Owner.tsx index 1417abc637831..600be23efbe12 100644 --- a/frontend/src/scenes/events/Owner.tsx +++ b/frontend/src/scenes/events/Owner.tsx @@ -6,12 +6,14 @@ export function Owner({ user, style = {} }: { user?: UserBasicType | null; style return ( <> {user?.uuid ? ( -
+
- {user.first_name} + + {user.first_name} +
) : ( - + No owner )} diff --git a/frontend/src/scenes/experiments/ExperimentImplementationDetails.tsx b/frontend/src/scenes/experiments/ExperimentImplementationDetails.tsx index 9653d430b1682..8582017bb7bcb 100644 --- a/frontend/src/scenes/experiments/ExperimentImplementationDetails.tsx +++ b/frontend/src/scenes/experiments/ExperimentImplementationDetails.tsx @@ -111,7 +111,7 @@ export function ExperimentImplementationDetails({ experiment }: ExperimentImplem title={Feature flag usage and implementation} className="experiment-implementation-details" > -
+
Variant group ) : ( - + Unknown field type "{fieldConfig.type}".
You may need to upgrade PostHog! diff --git a/frontend/src/scenes/plugins/plugin/PluginImage.tsx b/frontend/src/scenes/plugins/plugin/PluginImage.tsx index 67120a11074d8..9fec8b6275e9e 100644 --- a/frontend/src/scenes/plugins/plugin/PluginImage.tsx +++ b/frontend/src/scenes/plugins/plugin/PluginImage.tsx @@ -42,6 +42,7 @@ export function PluginImage({ ) : (
{type} + return {type} } const columns: LemonTableColumns> = [ diff --git a/frontend/src/scenes/plugins/source/PluginSource.tsx b/frontend/src/scenes/plugins/source/PluginSource.tsx index 0b2ba5fd675b1..0d016a06a7ba1 100644 --- a/frontend/src/scenes/plugins/source/PluginSource.tsx +++ b/frontend/src/scenes/plugins/source/PluginSource.tsx @@ -80,7 +80,7 @@ export function PluginSource({ title={pluginSourceLoading ? 'Loading...' : `Edit App: ${name}`} placement={placement ?? 'left'} footer={ -
+
@@ -126,7 +126,7 @@ export function PluginSource({ }} /> {!value && createDefaultPluginSource(name)[currentFile] ? ( -
+
diff --git a/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx b/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx index 1e5e0ad81b897..382a157bbbf6f 100644 --- a/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/InstalledAppsReorderModal.tsx @@ -16,6 +16,7 @@ const MinimalAppView = ({ plugin, order }: { plugin: PluginTypeWithConfig; order
+
+
- {/* eslint-disable-next-line react/forbid-dom-props */}
{/* eslint-disable-next-line react/forbid-dom-props */} diff --git a/frontend/src/scenes/session-recordings/player/icons.tsx b/frontend/src/scenes/session-recordings/player/icons.tsx index 6fff14ba753b5..fa8a79d631150 100644 --- a/frontend/src/scenes/session-recordings/player/icons.tsx +++ b/frontend/src/scenes/session-recordings/player/icons.tsx @@ -10,7 +10,7 @@ export function IconWindowOld({ value, className = '', size = 'medium' }: IconWi const shortValue = typeof value === 'number' ? value : String(value).charAt(0) return (
- + {shortValue}

{title}

{description &&

{description}

} @@ -694,13 +699,7 @@ export default function SurveyEdit(): JSX.Element { description="Use the PostHog API to show/hide your survey programmatically" value={SurveyType.API} > -
+
@@ -924,14 +923,12 @@ export default function SurveyEdit(): JSX.Element { />
-
-
- setSelectedQuestion(preview)} - /> -
+
+ setSelectedQuestion(preview)} + />
) diff --git a/frontend/src/scenes/surveys/SurveyFormAppearance.tsx b/frontend/src/scenes/surveys/SurveyFormAppearance.tsx index 5ff65153a4508..4ce32f42d6192 100644 --- a/frontend/src/scenes/surveys/SurveyFormAppearance.tsx +++ b/frontend/src/scenes/surveys/SurveyFormAppearance.tsx @@ -17,58 +17,54 @@ export function SurveyFormAppearance({ }: SurveyFormAppearanceProps): JSX.Element { const showThankYou = survey.appearance.displayThankYouMessage && activePreview >= survey.questions.length - return ( -
- {survey.type !== SurveyType.API ? ( - <> - {showThankYou ? ( - - ) : ( - 1 ? { submitButtonText: 'Next' } : null), - }} - /> - )} - { - setActivePreview(activePreview) - }} - className="mt-4 whitespace-nowrap" - fullWidth - value={activePreview} - options={[ - ...survey.questions.map((question, index) => ({ - label: `${index + 1}. ${question.question ?? ''}`, - value: index, - })), - ...(survey.appearance.displayThankYouMessage - ? [ - { - label: `${survey.questions.length + 1}. Confirmation message`, - value: survey.questions.length, - }, - ] - : []), - ]} - /> - + return survey.type !== SurveyType.API ? ( + <> + {showThankYou ? ( + ) : ( -
-

API survey response

- -
+ 1 ? { submitButtonText: 'Next' } : null), + }} + /> )} + { + setActivePreview(activePreview) + }} + className="mt-4 whitespace-nowrap" + fullWidth + value={activePreview} + options={[ + ...survey.questions.map((question, index) => ({ + label: `${index + 1}. ${question.question ?? ''}`, + value: index, + })), + ...(survey.appearance.displayThankYouMessage + ? [ + { + label: `${survey.questions.length + 1}. Confirmation message`, + value: survey.questions.length, + }, + ] + : []), + ]} + /> + + ) : ( +
+

API survey response

+
) } diff --git a/frontend/src/scenes/surveys/SurveyView.tsx b/frontend/src/scenes/surveys/SurveyView.tsx index 0da16f745b4ab..334027e67ba0d 100644 --- a/frontend/src/scenes/surveys/SurveyView.tsx +++ b/frontend/src/scenes/surveys/SurveyView.tsx @@ -213,7 +213,7 @@ export function SurveyView({ id }: { id: string }): JSX.Element {
)} {survey.type !== SurveyType.API ? ( -
+
@@ -121,6 +122,7 @@ export function UsersStackedBar({ surveyUserStats }: { surveyUserStats: SurveyUs ({ count, label, style }) => count > 0 && (
+ {/* eslint-disable-next-line react/forbid-dom-props */}
{`${label} (${( (count / total) * @@ -345,6 +347,7 @@ export function SingleChoiceQuestionPieChart({ >
{`${labels[i]}`} diff --git a/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx b/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx index bb26ecec52e83..9fe2c882f8dd1 100644 --- a/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx +++ b/frontend/src/scenes/trends/viz/ActionsLineGraph.tsx @@ -14,6 +14,8 @@ import { isInsightVizNode, isLifecycleQuery } from '~/queries/utils' import { DataTableNode, NodeKind } from '~/queries/schema' import { combineUrl, router } from 'kea-router' import { urls } from 'scenes/urls' +import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { FEATURE_FLAGS } from 'lib/constants' export function ActionsLineGraph({ inSharedMode = false, @@ -21,6 +23,7 @@ export function ActionsLineGraph({ context, }: ChartParams): JSX.Element | null { const { insightProps, hiddenLegendKeys } = useValues(insightLogic) + const { featureFlags } = useValues(featureFlagLogic) const { query } = useValues(insightDataLogic(insightProps)) const { indexedResults, @@ -82,7 +85,15 @@ export function ActionsLineGraph({ const day = dataset?.days?.[index] ?? '' const label = dataset?.label ?? dataset?.labels?.[index] ?? '' - if (isLifecycle && query && isInsightVizNode(query) && isLifecycleQuery(query.source)) { + const hogQLInsightsFlagEnabled = Boolean(featureFlags[FEATURE_FLAGS.HOGQL_INSIGHTS]) + + if ( + hogQLInsightsFlagEnabled && + isLifecycle && + query && + isInsightVizNode(query) && + isLifecycleQuery(query.source) + ) { const newQuery: DataTableNode = { kind: NodeKind.DataTableNode, full: true, diff --git a/package.json b/package.json index 86ae28bf5e5a0..431ec59a87561 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@microlink/react-json-view": "^1.21.3", "@monaco-editor/react": "4.4.6", "@posthog/icons": "0.1.25", - "@posthog/plugin-scaffold": "^1.4.3", + "@posthog/plugin-scaffold": "^1.4.4", "@react-hook/size": "^2.1.2", "@rrweb/types": "^2.0.0-alpha.11", "@sentry/react": "7.22.0", diff --git a/plugin-server/package.json b/plugin-server/package.json index 8608c8a92688e..0770f481d8a3a 100644 --- a/plugin-server/package.json +++ b/plugin-server/package.json @@ -49,7 +49,7 @@ "@maxmind/geoip2-node": "^3.4.0", "@posthog/clickhouse": "^1.7.0", "@posthog/plugin-contrib": "^0.0.5", - "@posthog/plugin-scaffold": "1.4.3", + "@posthog/plugin-scaffold": "1.4.4", "@sentry/node": "^7.49.0", "@sentry/profiling-node": "^0.3.0", "@sentry/tracing": "^7.17.4", diff --git a/plugin-server/pnpm-lock.yaml b/plugin-server/pnpm-lock.yaml index c061059038306..a58ab8c3b5e1c 100644 --- a/plugin-server/pnpm-lock.yaml +++ b/plugin-server/pnpm-lock.yaml @@ -53,8 +53,8 @@ dependencies: specifier: ^0.0.5 version: 0.0.5 '@posthog/plugin-scaffold': - specifier: 1.4.3 - version: 1.4.3 + specifier: 1.4.4 + version: 1.4.4 '@sentry/node': specifier: ^7.49.0 version: 7.49.0 @@ -3611,8 +3611,8 @@ packages: resolution: {integrity: sha512-ic2JsfFUdLGF+fGYJPatWEB6gEFNoD89qz92FN1RE2QfLpr6YdyPNuMowzahya3hfC/jaLZ8QdPG/j5pSOgT7A==} dev: false - /@posthog/plugin-scaffold@1.4.3: - resolution: {integrity: sha512-fzWJ8lvAUlbTcffaFVcfhV9QiAVGL4j1OnXDc+p+bqWMP/yTwvljXDiwV96w3mti9r40E1NBz/yQRykyB78+EA==} + /@posthog/plugin-scaffold@1.4.4: + resolution: {integrity: sha512-3z1ENm1Ys5lEQil0H7TVOqHvD24+ydiZFk5hggpbHRx1iOxAK+Eu5qFyAROwPUcCo7NOYjmH2xL1C4B1vaHilg==} dependencies: '@maxmind/geoip2-node': 3.5.0 dev: false diff --git a/plugin-server/src/utils/db/utils.ts b/plugin-server/src/utils/db/utils.ts index ca9035e29fcf6..a4f940defdefb 100644 --- a/plugin-server/src/utils/db/utils.ts +++ b/plugin-server/src/utils/db/utils.ts @@ -2,6 +2,7 @@ import { Properties } from '@posthog/plugin-scaffold' import * as Sentry from '@sentry/node' import { ProducerRecord } from 'kafkajs' import { DateTime } from 'luxon' +import { Counter } from 'prom-client' import { defaultConfig } from '../../config/config' import { KAFKA_PERSON } from '../../config/kafka-topics' @@ -150,6 +151,7 @@ export function shouldStoreLog( export function safeClickhouseString(str: string): string { // character is a surrogate return str.replace(/[\ud800-\udfff]/gu, (match) => { + surrogatesSubstitutedCounter.inc() const res = JSON.stringify(match) return res.slice(1, res.length - 1) + `\\` }) @@ -173,3 +175,8 @@ export function sanitizeJsonbValue(value: any): any { return value } } + +export const surrogatesSubstitutedCounter = new Counter({ + name: 'surrogates_substituted_total', + help: 'Stray UTF16 surrogates detected and removed from user input.', +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f80eb20b9d2c6..ab75dfdb562ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,8 +39,8 @@ dependencies: specifier: 0.1.25 version: 0.1.25(react-dom@18.2.0)(react@18.2.0) '@posthog/plugin-scaffold': - specifier: ^1.4.3 - version: 1.4.3 + specifier: ^1.4.4 + version: 1.4.4 '@react-hook/size': specifier: ^2.1.2 version: 2.1.2(react@18.2.0) @@ -3367,8 +3367,8 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@posthog/plugin-scaffold@1.4.3: - resolution: {integrity: sha512-fzWJ8lvAUlbTcffaFVcfhV9QiAVGL4j1OnXDc+p+bqWMP/yTwvljXDiwV96w3mti9r40E1NBz/yQRykyB78+EA==} + /@posthog/plugin-scaffold@1.4.4: + resolution: {integrity: sha512-3z1ENm1Ys5lEQil0H7TVOqHvD24+ydiZFk5hggpbHRx1iOxAK+Eu5qFyAROwPUcCo7NOYjmH2xL1C4B1vaHilg==} dependencies: '@maxmind/geoip2-node': 3.5.0 dev: false diff --git a/posthog/api/site_app.py b/posthog/api/site_app.py index 5333980206a70..6704ff0c7f534 100644 --- a/posthog/api/site_app.py +++ b/posthog/api/site_app.py @@ -34,5 +34,5 @@ def get_site_app(request: HttpRequest, id: int, token: str, hash: str) -> HttpRe "Unable to serve site app source code.", code="missing_site_app_source", type="server_error", - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + status_code=status.HTTP_404_NOT_FOUND, ) diff --git a/posthog/api/utils.py b/posthog/api/utils.py index 298908f1ccbe1..1953b86029c16 100644 --- a/posthog/api/utils.py +++ b/posthog/api/utils.py @@ -1,18 +1,19 @@ -from ipaddress import ip_address import json import re -from enum import Enum, auto import socket +import urllib.parse +from enum import Enum, auto +from ipaddress import ip_address from typing import List, Literal, Optional, Union, Tuple from uuid import UUID import structlog from django.core.exceptions import RequestDataTooBig from django.db.models import QuerySet +from prometheus_client import Counter from rest_framework import request, status from rest_framework.exceptions import ValidationError from statshog.defaults.django import statsd -import urllib.parse from posthog.constants import EventDefinitionType from posthog.exceptions import RequestParsingError, generate_exception_response @@ -230,11 +231,17 @@ def check_definition_ids_inclusion_field_sql( SURROGATE_REGEX = re.compile("([\ud800-\udfff])") +SURROGATES_SUBSTITUTED_COUNTER = Counter( + "surrogates_substituted_total", + "Stray UTF16 surrogates detected and removed from user input.", +) + # keep in sync with posthog/plugin-server/src/utils/db/utils.ts::safeClickhouseString def safe_clickhouse_string(s: str) -> str: matches = SURROGATE_REGEX.findall(s or "") for match in matches: + SURROGATES_SUBSTITUTED_COUNTER.inc() s = s.replace(match, match.encode("unicode_escape").decode("utf8")) return s diff --git a/unit.json b/unit.json index 34f66bff1ba8a..2cb730ff1d428 100644 --- a/unit.json +++ b/unit.json @@ -2,15 +2,56 @@ "listeners": { "*:8000": { "pass": "applications/posthog" + }, + "*:8001": { + "pass": "routes/metrics" + }, + "*:8081": { + "pass": "routes/status" } }, + "routes": { + "metrics": [ + { + "match": { + "uri": [ + "/metrics" + ] + }, + "action": { + "pass": "applications/metrics" + } + }, + ], + "status": [ + { + "match": { + "uri": [ + "/status" + ] + }, + "action": { + "proxy": "http://unix:/var/run/control.unit.sock" + } + }, + ], + }, "applications": { "posthog": { "type": "python 3.10", "processes": 1, "working_directory": "/code", "path": ".", - "module": "posthog.wsgi" - } + "module": "posthog.wsgi", + "user": "nobody" + }, + "metrics": { + "type": "python 3.10", + "processes": 1, + "working_directory": "/code/bin", + "path": ".", + "module": "unit_metrics", + "user": "nobody" + }, } }