diff --git a/.eslintrc.js b/.eslintrc.js index c2a5768e9c033..c7333fd0fd94d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,12 +27,12 @@ module.exports = { }, extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', 'plugin:react/recommended', 'plugin:eslint-comments/recommended', 'plugin:storybook/recommended', - 'prettier', 'plugin:compat/recommended', + 'prettier', ], globals, parser: '@typescript-eslint/parser', @@ -42,6 +42,7 @@ module.exports = { }, ecmaVersion: 2018, sourceType: 'module', + project: 'tsconfig.json' }, plugins: [ 'prettier', @@ -84,7 +85,27 @@ module.exports = { '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/require-await': 'off', // TODO: Enable - this rule is useful, but doesn't have an autofix + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-enum-comparison': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/explicit-function-return-type': [ + 'error', + { + allowExpressions: true, + }, + ], + '@typescript-eslint/explicit-module-boundary-types': [ + 'error', + { + allowArgumentsExplicitlyTypedAsAny: true, + }, + ], curly: 'error', 'no-restricted-imports': [ 'error', @@ -242,43 +263,29 @@ module.exports = { ...globals, given: 'readonly', }, + rules: { + // The below complains needlessly about expect(api.createInvite).toHaveBeenCalledWith(...) + '@typescript-eslint/unbound-method': 'off', + } }, { // disable these rules for files generated by kea-typegen files: ['*Type.ts', '*Type.tsx'], rules: { - '@typescript-eslint/no-explicit-any': ['off'], + 'no-restricted-imports': 'off', '@typescript-eslint/ban-types': ['off'], }, }, - { - // enable the rule specifically for TypeScript files - files: ['*.ts', '*.tsx'], - rules: { - '@typescript-eslint/no-explicit-any': ['off'], - '@typescript-eslint/explicit-function-return-type': [ - 'error', - { - allowExpressions: true, - }, - ], - '@typescript-eslint/explicit-module-boundary-types': [ - 'error', - { - allowArgumentsExplicitlyTypedAsAny: true, - }, - ], - }, - }, { files: ['*.js'], rules: { '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', }, }, { files: 'eslint-rules/**/*', - extends: ['eslint:recommended'], rules: { '@typescript-eslint/no-var-requires': 'off', }, diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml index 3095150406246..d290a0594bbf0 100644 --- a/.github/workflows/ci-frontend.yml +++ b/.github/workflows/ci-frontend.yml @@ -49,15 +49,15 @@ jobs: - name: Check formatting with prettier run: pnpm prettier:check - - name: Lint with ESLint - run: pnpm lint:js - - name: Lint with Stylelint run: pnpm lint:css - name: Generate logic types and run typescript with strict run: pnpm typegen:write && pnpm typescript:check + - name: Lint with ESLint + run: pnpm lint:js + - name: Check if "schema.json" is up to date run: pnpm schema:build:json && git diff --exit-code diff --git a/cypress/e2e/actions.cy.ts b/cypress/e2e/actions.cy.ts index 9819b7d02cdab..356607c64bf8f 100644 --- a/cypress/e2e/actions.cy.ts +++ b/cypress/e2e/actions.cy.ts @@ -5,7 +5,7 @@ const createAction = (actionName: string): void => { cy.get('[data-attr=action-name-create]').should('exist') cy.get('[data-attr=action-name-create]').type(actionName) - cy.get('.ant-radio-group > :nth-child(3)').click() + cy.get('.LemonSegmentedButton > ul > :nth-child(3)').click() cy.get('[data-attr=edit-action-url-input]').click().type(Cypress.config().baseUrl) cy.get('[data-attr=save-action-button]').click() diff --git a/ee/api/test/test_billing.py b/ee/api/test/test_billing.py index 87838d0b39dcc..88addd2d7f416 100644 --- a/ee/api/test/test_billing.py +++ b/ee/api/test/test_billing.py @@ -43,6 +43,7 @@ def create_missing_billing_customer(**kwargs) -> CustomerInfo: usage_summary={ "events": {"limit": None, "usage": 0}, "recordings": {"limit": None, "usage": 0}, + "rows_synced": {"limit": None, "usage": 0}, }, free_trial_until=None, available_features=[], @@ -96,6 +97,7 @@ def create_billing_customer(**kwargs) -> CustomerInfo: usage_summary={ "events": {"limit": None, "usage": 0}, "recordings": {"limit": None, "usage": 0}, + "rows_synced": {"limit": None, "usage": 0}, }, free_trial_until=None, ) @@ -292,6 +294,7 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma "usage_summary": { "events": {"limit": None, "usage": 0}, "recordings": {"limit": None, "usage": 0}, + "rows_synced": {"limit": None, "usage": 0}, }, "free_trial_until": None, } @@ -363,6 +366,7 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma "usage_summary": { "events": {"limit": None, "usage": 0}, "recordings": {"limit": None, "usage": 0}, + "rows_synced": {"limit": None, "usage": 0}, }, "free_trial_until": None, "current_total_amount_usd": "0.00", @@ -521,6 +525,11 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma "todays_usage": 0, "usage": 0, }, + "rows_synced": { + "limit": None, + "todays_usage": 0, + "usage": 0, + }, "period": ["2022-10-07T11:12:48", "2022-11-07T11:12:48"], } @@ -556,6 +565,11 @@ def mock_implementation_missing_customer(url: str, headers: Any = None, params: "todays_usage": 0, "usage": 0, }, + "rows_synced": { + "limit": None, + "todays_usage": 0, + "usage": 0, + }, "period": ["2022-10-07T11:12:48", "2022-11-07T11:12:48"], } assert self.organization.customer_id == "cus_123" @@ -613,5 +627,6 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma assert self.organization.usage == { "events": {"limit": None, "usage": 0, "todays_usage": 0}, "recordings": {"limit": None, "usage": 0, "todays_usage": 0}, + "rows_synced": {"limit": None, "usage": 0, "todays_usage": 0}, "period": ["2022-10-07T11:12:48", "2022-11-07T11:12:48"], } diff --git a/ee/billing/billing_manager.py b/ee/billing/billing_manager.py index c626083460ef4..5a8119c57df9b 100644 --- a/ee/billing/billing_manager.py +++ b/ee/billing/billing_manager.py @@ -225,6 +225,7 @@ def update_org_details(self, organization: Organization, billing_status: Billing usage_info = OrganizationUsageInfo( events=usage_summary["events"], recordings=usage_summary["recordings"], + rows_synced=usage_summary.get("rows_synced", None), period=[ data["billing_period"]["current_period_start"], data["billing_period"]["current_period_end"], diff --git a/ee/billing/quota_limiting.py b/ee/billing/quota_limiting.py index ae6eefcc0b77a..ef3e12a421575 100644 --- a/ee/billing/quota_limiting.py +++ b/ee/billing/quota_limiting.py @@ -17,6 +17,7 @@ convert_team_usage_rows_to_dict, get_teams_with_billable_event_count_in_period, get_teams_with_recording_count_in_period, + get_teams_with_rows_synced_in_period, ) from posthog.utils import get_current_day @@ -26,11 +27,13 @@ class QuotaResource(Enum): EVENTS = "events" RECORDINGS = "recordings" + ROWS_SYNCED = "rows_synced" OVERAGE_BUFFER = { QuotaResource.EVENTS: 0, QuotaResource.RECORDINGS: 1000, + QuotaResource.ROWS_SYNCED: 0, } @@ -53,7 +56,7 @@ def remove_limited_team_tokens(resource: QuotaResource, tokens: List[str]) -> No @cache_for(timedelta(seconds=30), background_refresh=True) -def list_limited_team_tokens(resource: QuotaResource) -> List[str]: +def list_limited_team_attributes(resource: QuotaResource) -> List[str]: now = timezone.now() redis_client = get_client() results = redis_client.zrangebyscore(f"{QUOTA_LIMITER_CACHE_KEY}{resource.value}", min=now.timestamp(), max="+inf") @@ -63,6 +66,7 @@ def list_limited_team_tokens(resource: QuotaResource) -> List[str]: class UsageCounters(TypedDict): events: int recordings: int + rows_synced: int def org_quota_limited_until(organization: Organization, resource: QuotaResource) -> Optional[int]: @@ -70,6 +74,8 @@ def org_quota_limited_until(organization: Organization, resource: QuotaResource) return None summary = organization.usage.get(resource.value, {}) + if not summary: + return None usage = summary.get("usage", 0) todays_usage = summary.get("todays_usage", 0) limit = summary.get("limit") @@ -93,19 +99,34 @@ def sync_org_quota_limits(organization: Organization): if not organization.usage: return None - team_tokens: List[str] = [x for x in list(organization.teams.values_list("api_token", flat=True)) if x] - - if not team_tokens: - capture_exception(Exception(f"quota_limiting: No team tokens found for organization: {organization.id}")) - return - - for resource in [QuotaResource.EVENTS, QuotaResource.RECORDINGS]: + for resource in [QuotaResource.EVENTS, QuotaResource.RECORDINGS, QuotaResource.ROWS_SYNCED]: + team_attributes = get_team_attribute_by_quota_resource(organization, resource) quota_limited_until = org_quota_limited_until(organization, resource) if quota_limited_until: - add_limited_team_tokens(resource, {x: quota_limited_until for x in team_tokens}) + add_limited_team_tokens(resource, {x: quota_limited_until for x in team_attributes}) else: - remove_limited_team_tokens(resource, team_tokens) + remove_limited_team_tokens(resource, team_attributes) + + +def get_team_attribute_by_quota_resource(organization: Organization, resource: QuotaResource): + if resource in [QuotaResource.EVENTS, QuotaResource.RECORDINGS]: + team_tokens: List[str] = [x for x in list(organization.teams.values_list("api_token", flat=True)) if x] + + if not team_tokens: + capture_exception(Exception(f"quota_limiting: No team tokens found for organization: {organization.id}")) + return + + return team_tokens + + if resource == QuotaResource.ROWS_SYNCED: + team_ids: List[str] = [x for x in list(organization.teams.values_list("id", flat=True)) if x] + + if not team_ids: + capture_exception(Exception(f"quota_limiting: No team ids found for organization: {organization.id}")) + return + + return team_ids def set_org_usage_summary( @@ -125,8 +146,10 @@ def set_org_usage_summary( new_usage = copy.deepcopy(new_usage) - for field in ["events", "recordings"]: + for field in ["events", "recordings", "rows_synced"]: resource_usage = new_usage[field] # type: ignore + if not resource_usage: + continue if todays_usage: resource_usage["todays_usage"] = todays_usage[field] # type: ignore @@ -155,6 +178,9 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, teams_with_recording_count_in_period=convert_team_usage_rows_to_dict( get_teams_with_recording_count_in_period(period_start, period_end) ), + teams_with_rows_synced_in_period=convert_team_usage_rows_to_dict( + get_teams_with_rows_synced_in_period(period_start, period_end) + ), ) teams: Sequence[Team] = list( @@ -171,6 +197,7 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, team_report = UsageCounters( events=all_data["teams_with_event_count_in_period"].get(team.id, 0), recordings=all_data["teams_with_recording_count_in_period"].get(team.id, 0), + rows_synced=all_data["teams_with_rows_synced_in_period"].get(team.id, 0), ) org_id = str(team.organization.id) @@ -183,7 +210,7 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, for field in team_report: org_report[field] += team_report[field] # type: ignore - quota_limited_orgs: Dict[str, Dict[str, int]] = {"events": {}, "recordings": {}} + quota_limited_orgs: Dict[str, Dict[str, int]] = {"events": {}, "recordings": {}, "rows_synced": {}} # We find all orgs that should be rate limited for org_id, todays_report in todays_usage_report.items(): @@ -195,7 +222,7 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, if set_org_usage_summary(org, todays_usage=todays_report): org.save(update_fields=["usage"]) - for field in ["events", "recordings"]: + for field in ["events", "recordings", "rows_synced"]: quota_limited_until = org_quota_limited_until(org, QuotaResource(field)) if quota_limited_until: @@ -207,12 +234,13 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, previously_quota_limited_team_tokens: Dict[str, Dict[str, int]] = { "events": {}, "recordings": {}, + "rows_synced": {}, } for field in quota_limited_orgs: - previously_quota_limited_team_tokens[field] = list_limited_team_tokens(QuotaResource(field)) + previously_quota_limited_team_tokens[field] = list_limited_team_attributes(QuotaResource(field)) - quota_limited_teams: Dict[str, Dict[str, int]] = {"events": {}, "recordings": {}} + quota_limited_teams: Dict[str, Dict[str, int]] = {"events": {}, "recordings": {}, "rows_synced": {}} # Convert the org ids to team tokens for team in teams: @@ -233,6 +261,7 @@ def update_all_org_billing_quotas(dry_run: bool = False) -> Dict[str, Dict[str, properties = { "quota_limited_events": quota_limited_orgs["events"].get(org_id, None), "quota_limited_recordings": quota_limited_orgs["events"].get(org_id, None), + "quota_limited_rows_synced": quota_limited_orgs["rows_synced"].get(org_id, None), } report_organization_action( diff --git a/ee/billing/test/test_quota_limiting.py b/ee/billing/test/test_quota_limiting.py index 3bdc70a06df9e..b8e68c235b2c5 100644 --- a/ee/billing/test/test_quota_limiting.py +++ b/ee/billing/test/test_quota_limiting.py @@ -9,7 +9,7 @@ from ee.billing.quota_limiting import ( QUOTA_LIMITER_CACHE_KEY, QuotaResource, - list_limited_team_tokens, + list_limited_team_attributes, org_quota_limited_until, replace_limited_team_tokens, set_org_usage_summary, @@ -47,15 +47,18 @@ def test_billing_rate_limit_not_set_if_missing_org_usage(self) -> None: result = update_all_org_billing_quotas() assert result["events"] == {} assert result["recordings"] == {} + assert result["rows_synced"] == {} assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}events", 0, -1) == [] assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}recordings", 0, -1) == [] + assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}rows_synced", 0, -1) == [] def test_billing_rate_limit(self) -> None: with self.settings(USE_TZ=False): self.organization.usage = { "events": {"usage": 99, "limit": 100}, "recordings": {"usage": 1, "limit": 100}, + "rows_synced": {"usage": 5, "limit": 100}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } self.organization.save() @@ -77,16 +80,19 @@ def test_billing_rate_limit(self) -> None: org_id = str(self.organization.id) assert result["events"] == {org_id: 1612137599} assert result["recordings"] == {} + assert result["rows_synced"] == {} assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}events", 0, -1) == [ self.team.api_token.encode("UTF-8") ] assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}recordings", 0, -1) == [] + assert self.redis_client.zrange(f"{QUOTA_LIMITER_CACHE_KEY}rows_synced", 0, -1) == [] self.organization.refresh_from_db() assert self.organization.usage == { "events": {"usage": 99, "limit": 100, "todays_usage": 10}, "recordings": {"usage": 1, "limit": 100, "todays_usage": 0}, + "rows_synced": {"usage": 5, "limit": 100, "todays_usage": 0}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } @@ -94,6 +100,7 @@ def test_set_org_usage_summary_updates_correctly(self): self.organization.usage = { "events": {"usage": 99, "limit": 100}, "recordings": {"usage": 1, "limit": 100}, + "rows_synced": {"usage": 5, "limit": 100}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } self.organization.save() @@ -101,6 +108,7 @@ def test_set_org_usage_summary_updates_correctly(self): new_usage = dict( events={"usage": 100, "limit": 100}, recordings={"usage": 2, "limit": 100}, + rows_synced={"usage": 6, "limit": 100}, period=[ "2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z", @@ -112,6 +120,7 @@ def test_set_org_usage_summary_updates_correctly(self): assert self.organization.usage == { "events": {"usage": 100, "limit": 100, "todays_usage": 0}, "recordings": {"usage": 2, "limit": 100, "todays_usage": 0}, + "rows_synced": {"usage": 6, "limit": 100, "todays_usage": 0}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } @@ -119,6 +128,7 @@ def test_set_org_usage_summary_does_nothing_if_the_same(self): self.organization.usage = { "events": {"usage": 99, "limit": 100, "todays_usage": 10}, "recordings": {"usage": 1, "limit": 100, "todays_usage": 11}, + "rows_synced": {"usage": 5, "limit": 100, "todays_usage": 11}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } self.organization.save() @@ -126,6 +136,7 @@ def test_set_org_usage_summary_does_nothing_if_the_same(self): new_usage = dict( events={"usage": 99, "limit": 100}, recordings={"usage": 1, "limit": 100}, + rows_synced={"usage": 5, "limit": 100}, period=[ "2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z", @@ -137,6 +148,7 @@ def test_set_org_usage_summary_does_nothing_if_the_same(self): assert self.organization.usage == { "events": {"usage": 99, "limit": 100, "todays_usage": 10}, "recordings": {"usage": 1, "limit": 100, "todays_usage": 11}, + "rows_synced": {"usage": 5, "limit": 100, "todays_usage": 11}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } @@ -144,15 +156,19 @@ def test_set_org_usage_summary_updates_todays_usage(self): self.organization.usage = { "events": {"usage": 99, "limit": 100, "todays_usage": 10}, "recordings": {"usage": 1, "limit": 100, "todays_usage": 11}, + "rows_synced": {"usage": 5, "limit": 100, "todays_usage": 11}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } self.organization.save() - assert set_org_usage_summary(self.organization, todays_usage={"events": 20, "recordings": 21}) + assert set_org_usage_summary( + self.organization, todays_usage={"events": 20, "recordings": 21, "rows_synced": 21} + ) assert self.organization.usage == { "events": {"usage": 99, "limit": 100, "todays_usage": 20}, "recordings": {"usage": 1, "limit": 100, "todays_usage": 21}, + "rows_synced": {"usage": 5, "limit": 100, "todays_usage": 21}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } @@ -163,6 +179,7 @@ def test_org_quota_limited_until(self): self.organization.usage = { "events": {"usage": 99, "limit": 100}, "recordings": {"usage": 1, "limit": 100}, + "rows_synced": {"usage": 99, "limit": 100}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } @@ -184,6 +201,11 @@ def test_org_quota_limited_until(self): self.organization.usage["recordings"]["usage"] = 1100 # Over limit + buffer assert org_quota_limited_until(self.organization, QuotaResource.RECORDINGS) == 1612137599 + assert org_quota_limited_until(self.organization, QuotaResource.ROWS_SYNCED) is None + + self.organization.usage["rows_synced"]["usage"] = 101 + assert org_quota_limited_until(self.organization, QuotaResource.ROWS_SYNCED) == 1612137599 + def test_over_quota_but_not_dropped_org(self): self.organization.usage = None assert org_quota_limited_until(self.organization, QuotaResource.EVENTS) is None @@ -191,12 +213,14 @@ def test_over_quota_but_not_dropped_org(self): self.organization.usage = { "events": {"usage": 100, "limit": 90}, "recordings": {"usage": 100, "limit": 90}, + "rows_synced": {"usage": 100, "limit": 90}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } self.organization.never_drop_data = True assert org_quota_limited_until(self.organization, QuotaResource.EVENTS) is None assert org_quota_limited_until(self.organization, QuotaResource.RECORDINGS) is None + assert org_quota_limited_until(self.organization, QuotaResource.ROWS_SYNCED) is None # reset for subsequent tests self.organization.never_drop_data = False @@ -208,21 +232,32 @@ def test_sync_org_quota_limits(self): now = timezone.now().timestamp() replace_limited_team_tokens(QuotaResource.EVENTS, {"1234": now + 10000}) + replace_limited_team_tokens(QuotaResource.ROWS_SYNCED, {"1337": now + 10000}) self.organization.usage = { "events": {"usage": 99, "limit": 100}, "recordings": {"usage": 1, "limit": 100}, + "rows_synced": {"usage": 35, "limit": 100}, "period": ["2021-01-01T00:00:00Z", "2021-01-31T23:59:59Z"], } sync_org_quota_limits(self.organization) - assert list_limited_team_tokens(QuotaResource.EVENTS) == ["1234"] + assert list_limited_team_attributes(QuotaResource.EVENTS) == ["1234"] + assert list_limited_team_attributes(QuotaResource.ROWS_SYNCED) == ["1337"] self.organization.usage["events"]["usage"] = 120 + self.organization.usage["rows_synced"]["usage"] = 120 sync_org_quota_limits(self.organization) - assert sorted(list_limited_team_tokens(QuotaResource.EVENTS)) == sorted( + assert sorted(list_limited_team_attributes(QuotaResource.EVENTS)) == sorted( ["1234", self.team.api_token, other_team.api_token] ) + # rows_synced uses teams, not tokens + assert sorted(list_limited_team_attributes(QuotaResource.ROWS_SYNCED)) == sorted( + ["1337", str(self.team.pk), str(other_team.pk)] + ) + self.organization.usage["events"]["usage"] = 80 + self.organization.usage["rows_synced"]["usage"] = 36 sync_org_quota_limits(self.organization) - assert sorted(list_limited_team_tokens(QuotaResource.EVENTS)) == sorted(["1234"]) + assert sorted(list_limited_team_attributes(QuotaResource.EVENTS)) == sorted(["1234"]) + assert sorted(list_limited_team_attributes(QuotaResource.ROWS_SYNCED)) == sorted(["1337"]) diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-configuration.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-configuration.png new file mode 100644 index 0000000000000..94b53793cc3fe Binary files /dev/null and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-configuration.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-logs.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-logs.png new file mode 100644 index 0000000000000..f7ea3ec3cff02 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-logs.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-metrics.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-metrics.png new file mode 100644 index 0000000000000..16186b1e522ff Binary files /dev/null and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-app-metrics.png differ diff --git a/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.tsx b/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.tsx index 95be75be7a7b8..5899f59b82d47 100644 --- a/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.tsx +++ b/frontend/src/layout/FeaturePreviews/FeaturePreviewsModal.tsx @@ -121,9 +121,10 @@ function FeaturePreview({ feature }: { feature: EnrichedEarlyAccessFeature }): J /> { - await submitEarlyAccessFeatureFeedback(feedback) - setFeedback('') + onClick={() => { + void submitEarlyAccessFeatureFeedback(feedback).then(() => { + setFeedback('') + }) }} loading={activeFeedbackFlagKeyLoading} fullWidth diff --git a/frontend/src/layout/navigation-3000/Navigation.tsx b/frontend/src/layout/navigation-3000/Navigation.tsx index 54d7f872ea407..8460d3e347288 100644 --- a/frontend/src/layout/navigation-3000/Navigation.tsx +++ b/frontend/src/layout/navigation-3000/Navigation.tsx @@ -6,7 +6,7 @@ import { CommandPalette } from 'lib/components/CommandPalette/CommandPalette' import { FlaggedFeature } from 'lib/components/FlaggedFeature' import { FEATURE_FLAGS } from 'lib/constants' import { ReactNode, useEffect } from 'react' -import { Scene, SceneConfig } from 'scenes/sceneTypes' +import { SceneConfig } from 'scenes/sceneTypes' import { Breadcrumbs } from './components/Breadcrumbs' import { Navbar } from './components/Navbar' @@ -20,7 +20,6 @@ export function Navigation({ sceneConfig, }: { children: ReactNode - scene: Scene | null sceneConfig: SceneConfig | null }): JSX.Element { useMountedLogic(themeLogic) diff --git a/frontend/src/layout/navigation-3000/components/Sidebar.tsx b/frontend/src/layout/navigation-3000/components/Sidebar.tsx index 52ae81c9aa1ac..c8bff277ca08f 100644 --- a/frontend/src/layout/navigation-3000/components/Sidebar.tsx +++ b/frontend/src/layout/navigation-3000/components/Sidebar.tsx @@ -8,7 +8,7 @@ import React, { useRef, useState } from 'react' import { useDebouncedCallback } from 'use-debounce' import { navigation3000Logic } from '../navigationLogic' -import { SidebarCategory, SidebarLogic, SidebarNavbarItem } from '../types' +import { SidebarLogic, SidebarNavbarItem } from '../types' import { KeyboardShortcut } from './KeyboardShortcut' import { NewItemButton } from './NewItemButton' import { pluralizeCategory, SidebarAccordion } from './SidebarAccordion' @@ -178,7 +178,7 @@ function SidebarContent({ return contents.length !== 1 ? ( <> - {(contents as SidebarCategory[]).map((accordion) => ( + {contents.map((accordion) => ( ))} diff --git a/frontend/src/layout/navigation-3000/components/SidebarList.tsx b/frontend/src/layout/navigation-3000/components/SidebarList.tsx index 120f96479e8d7..7380b85422915 100644 --- a/frontend/src/layout/navigation-3000/components/SidebarList.tsx +++ b/frontend/src/layout/navigation-3000/components/SidebarList.tsx @@ -308,7 +308,7 @@ function SidebarListItem({ item, validateName, active, style }: SidebarListItemP navigation3000Logic.actions.focusPreviousItem() e.preventDefault() } else if (e.key === 'Enter') { - save(newName || '').then(() => { + void save(newName || '').then(() => { // In the keyboard nav experience, we need to refocus the item once it's a link again setTimeout(() => ref.current?.focus(), 0) }) @@ -328,7 +328,7 @@ function SidebarListItem({ item, validateName, active, style }: SidebarListItemP }} onBlur={(e) => { if (e.relatedTarget?.ariaLabel === 'Save name') { - save(newName || '') + void save(newName || '') } else { cancel() } diff --git a/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx b/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx index 498537a210332..b9ab37d3552bf 100644 --- a/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx +++ b/frontend/src/layout/navigation-3000/sidebars/featureFlags.tsx @@ -80,7 +80,7 @@ export const featureFlagsSidebarLogic = kea([ items: [ { label: 'Edit', - to: urls.featureFlag(featureFlag.id as number), + to: urls.featureFlag(featureFlag.id), onClick: () => { featureFlagLogic({ id: featureFlag.id as number }).mount() featureFlagLogic({ @@ -108,8 +108,8 @@ export const featureFlagsSidebarLogic = kea([ }, { label: 'Copy flag key', - onClick: async () => { - await copyToClipboard(featureFlag.key, 'feature flag key') + onClick: () => { + void copyToClipboard(featureFlag.key, 'feature flag key') }, }, { @@ -130,7 +130,7 @@ export const featureFlagsSidebarLogic = kea([ { label: 'Delete feature flag', onClick: () => { - deleteWithUndo({ + void deleteWithUndo({ endpoint: `projects/${currentTeamId}/feature_flags`, object: { name: featureFlag.key, id: featureFlag.id }, callback: actions.loadFeatureFlags, diff --git a/frontend/src/layout/navigation-3000/sidebars/insights.ts b/frontend/src/layout/navigation-3000/sidebars/insights.ts index cfe758ec1778b..20312e0b8f139 100644 --- a/frontend/src/layout/navigation-3000/sidebars/insights.ts +++ b/frontend/src/layout/navigation-3000/sidebars/insights.ts @@ -88,7 +88,7 @@ export const insightsSidebarLogic = kea([ }, { onClick: () => { - deleteWithUndo({ + void deleteWithUndo({ object: insight, endpoint: `projects/${currentTeamId}/insights`, callback: actions.loadInsights, @@ -118,7 +118,7 @@ export const insightsSidebarLogic = kea([ for (let i = startIndex; i < startIndex + INSIGHTS_PER_PAGE; i++) { cache.requestedInsights[i] = true } - await savedInsightsLogic.actions.setSavedInsightsFilters( + await savedInsightsLogic.asyncActions.setSavedInsightsFilters( { page: Math.floor(startIndex / INSIGHTS_PER_PAGE) + 1 }, true, false diff --git a/frontend/src/layout/navigation/Navigation.tsx b/frontend/src/layout/navigation/Navigation.tsx index 6c1793dccbbcf..5dbdb204a2c5f 100644 --- a/frontend/src/layout/navigation/Navigation.tsx +++ b/frontend/src/layout/navigation/Navigation.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx' import { BillingAlertsV2 } from 'lib/components/BillingAlertsV2' import { ReactNode } from 'react' -import { Scene, SceneConfig } from 'scenes/sceneTypes' +import { SceneConfig } from 'scenes/sceneTypes' import { Breadcrumbs } from './Breadcrumbs/Breadcrumbs' import { ProjectNotice } from './ProjectNotice' @@ -10,16 +10,14 @@ import { TopBar } from './TopBar/TopBar' export function Navigation({ children, - scene, sceneConfig, }: { children: ReactNode - scene: Scene | null sceneConfig: SceneConfig | null }): JSX.Element { return (
- {scene !== Scene.Ingestion && } +
{ - updateCurrentTeam(altTeamForIngestion?.id, urls.ingestion()) + updateCurrentTeam(altTeamForIngestion?.id, urls.products()) }} data-attr="demo-project-alt-team-ingestion_link" > - ingestion wizard + onboarding wizard {' '} to get started with your own data. @@ -61,8 +63,11 @@ export function ProjectNotice(): JSX.Element | null { message: ( <> This project has no events yet. Go to the{' '} - - ingestion wizard + + onboarding wizard {' '} or grab your project API key/HTML snippet from{' '} @@ -72,7 +77,7 @@ export function ProjectNotice(): JSX.Element | null { ), action: { - to: '/ingestion', + to: urls.onboarding(ProductKey.PRODUCT_ANALYTICS), 'data-attr': 'demo-warning-cta', icon: , children: 'Go to wizard', diff --git a/frontend/src/layout/navigation/TopBar/announcementLogic.test.ts b/frontend/src/layout/navigation/TopBar/announcementLogic.test.ts index cb54fc1635d5a..562a5ec022e59 100644 --- a/frontend/src/layout/navigation/TopBar/announcementLogic.test.ts +++ b/frontend/src/layout/navigation/TopBar/announcementLogic.test.ts @@ -33,7 +33,7 @@ describe('announcementLogic', () => { }) it('hides announcements during the ingestion phase', async () => { - router.actions.push(urls.ingestion()) + router.actions.push(urls.products()) await expectLogic(logic).toMatchValues({ cloudAnnouncement: DEFAULT_CLOUD_ANNOUNCEMENT, shownAnnouncementType: null, diff --git a/frontend/src/layout/navigation/TopBar/announcementLogic.ts b/frontend/src/layout/navigation/TopBar/announcementLogic.ts index ca8cdc1aa630c..593ef92032830 100644 --- a/frontend/src/layout/navigation/TopBar/announcementLogic.ts +++ b/frontend/src/layout/navigation/TopBar/announcementLogic.ts @@ -4,7 +4,6 @@ import { FEATURE_FLAGS, OrganizationMembershipLevel } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import posthog from 'posthog-js' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' import { navigationLogic } from '../navigationLogic' @@ -87,7 +86,8 @@ export const announcementLogic = kea([ (closable && (closed || (relevantAnnouncementType && persistedClosedAnnouncements[relevantAnnouncementType]))) || // hide if already closed - pathname == urls.ingestion() // hide during the ingestion phase + pathname.includes('/onboarding') || + pathname.includes('/products') // hide during the onboarding phase ) { return null } diff --git a/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx b/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx index 2f133df24d41d..1dcf216b92566 100644 --- a/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx +++ b/frontend/src/layout/navigation/TopBar/notificationsLogic.tsx @@ -55,9 +55,9 @@ export const notificationsLogic = kea([ clearTimeout(values.pollTimeout) try { - const response = (await api.get( + const response = await api.get( `api/projects/${teamLogic.values.currentTeamId}/activity_log/important_changes` - )) as ChangesResponse + ) // we can't rely on automatic success action here because we swallow errors so always succeed actions.clearErrorCount() return response @@ -115,14 +115,17 @@ export const notificationsLogic = kea([ a.created_at.isAfter(b.created_at) ? a : b ).created_at actions.setMarkReadTimeout( - window.setTimeout(async () => { - await api.create( - `api/projects/${teamLogic.values.currentTeamId}/activity_log/bookmark_activity_notification`, - { - bookmark: bookmarkDate.toISOString(), - } - ) - actions.markAllAsRead(bookmarkDate.toISOString()) + window.setTimeout(() => { + void api + .create( + `api/projects/${teamLogic.values.currentTeamId}/activity_log/bookmark_activity_notification`, + { + bookmark: bookmarkDate.toISOString(), + } + ) + .then(() => { + actions.markAllAsRead(bookmarkDate.toISOString()) + }) }, MARK_READ_TIMEOUT) ) } diff --git a/frontend/src/lib/animations/animations.ts b/frontend/src/lib/animations/animations.ts index 7d30b6932498c..40551f4979cb1 100644 --- a/frontend/src/lib/animations/animations.ts +++ b/frontend/src/lib/animations/animations.ts @@ -33,7 +33,7 @@ async function fetchJson(url: string): Promise> { export async function getAnimationSource(animation: AnimationType): Promise> { if (!animationCache[animation]) { - if (!fetchCache[animation]) { + if (!(animation in fetchCache)) { fetchCache[animation] = fetchJson(animations[animation].url) } animationCache[animation] = await fetchCache[animation] diff --git a/frontend/src/lib/components/ActivationSidebar/activationLogic.ts b/frontend/src/lib/components/ActivationSidebar/activationLogic.ts index 7a0542c82202f..9d11bceabbcf3 100644 --- a/frontend/src/lib/components/ActivationSidebar/activationLogic.ts +++ b/frontend/src/lib/components/ActivationSidebar/activationLogic.ts @@ -13,7 +13,7 @@ import { urls } from 'scenes/urls' import { navigationLogic } from '~/layout/navigation/navigationLogic' import { dashboardsModel } from '~/models/dashboardsModel' -import { EventDefinitionType, TeamBasicType } from '~/types' +import { EventDefinitionType, ProductKey, TeamBasicType } from '~/types' import type { activationLogicType } from './activationLogicType' @@ -329,7 +329,7 @@ export const activationLogic = kea([ runTask: async ({ id }) => { switch (id) { case ActivationTasks.IngestFirstEvent: - router.actions.push(urls.ingestion()) + router.actions.push(urls.onboarding(ProductKey.PRODUCT_ANALYTICS)) break case ActivationTasks.InviteTeamMember: actions.showInviteModal() diff --git a/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx b/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx index d7d745063a9e4..a851a373c0975 100644 --- a/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx +++ b/frontend/src/lib/components/ActivityLog/activityLogLogic.tsx @@ -104,7 +104,7 @@ export const activityLogLogic = kea([ })), listeners(({ actions }) => ({ setPage: async (_, breakpoint) => { - await breakpoint() + breakpoint() actions.fetchActivity() }, })), diff --git a/frontend/src/lib/components/Animation/Animation.tsx b/frontend/src/lib/components/Animation/Animation.tsx index c6f499e7a866d..df6fbadc929c7 100644 --- a/frontend/src/lib/components/Animation/Animation.tsx +++ b/frontend/src/lib/components/Animation/Animation.tsx @@ -40,7 +40,7 @@ export function Animation({ // Actually fetch the animation. Uses a cache to avoid multiple requests for the same file. // Show a fallback spinner if failed to fetch. useEffect(() => { - let unmounted = false + let unmounted = false // Poor person's abort controller async function loadAnimation(): Promise { try { const source = await getAnimationSource(type) @@ -49,7 +49,7 @@ export function Animation({ !unmounted && setShowFallbackSpinner(true) } } - loadAnimation() + void loadAnimation() return () => { unmounted = true } diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightCard.tsx b/frontend/src/lib/components/Cards/InsightCard/InsightCard.tsx index 7dad423315b9f..eacf36fb137eb 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightCard.tsx +++ b/frontend/src/lib/components/Cards/InsightCard/InsightCard.tsx @@ -145,7 +145,7 @@ export interface InsightCardProps extends Resizeable, React.HTMLAttributes void removeFromDashboard?: () => void - deleteWithUndo?: () => void + deleteWithUndo?: () => Promise refresh?: () => void rename?: () => void duplicate?: () => void diff --git a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx index 6465a1f99fbfa..b5cc113df2e14 100644 --- a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx +++ b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx @@ -133,8 +133,10 @@ export function CodeSnippet({ } - onClick={async () => { - text && (await copyToClipboard(text, thing)) + onClick={() => { + if (text) { + void copyToClipboard(text, thing) + } }} size={compact ? 'small' : 'medium'} /> diff --git a/frontend/src/lib/components/CommandBar/searchBarLogic.ts b/frontend/src/lib/components/CommandBar/searchBarLogic.ts index 335dca58b843c..32708d8c535c1 100644 --- a/frontend/src/lib/components/CommandBar/searchBarLogic.ts +++ b/frontend/src/lib/components/CommandBar/searchBarLogic.ts @@ -87,7 +87,6 @@ export const searchBarLogic = kea([ }), listeners(({ values, actions }) => ({ openResult: ({ index }) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const result = values.searchResults![index] router.actions.push(urlForResult(result)) actions.hideCommandBar() diff --git a/frontend/src/lib/components/CommandPalette/CommandPalette.tsx b/frontend/src/lib/components/CommandPalette/CommandPalette.tsx index f09583346a446..20f801a408170 100644 --- a/frontend/src/lib/components/CommandPalette/CommandPalette.tsx +++ b/frontend/src/lib/components/CommandPalette/CommandPalette.tsx @@ -38,7 +38,7 @@ function _CommandPalette(): JSX.Element | null { useEventListener('keydown', (event) => { if (isSqueak && event.key === 'Enter') { - squeakAudio?.play() + void squeakAudio?.play() } else if (event.key === 'Escape') { event.preventDefault() // Return to previous flow diff --git a/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx b/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx index bb6e40c301f3d..2a87d26d1deea 100644 --- a/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx +++ b/frontend/src/lib/components/CommandPalette/DebugCHQueries.tsx @@ -11,7 +11,7 @@ import { LemonTable } from 'lib/lemon-ui/LemonTable' import { CodeSnippet, Language } from '../CodeSnippet' import type { debugCHQueriesLogicType } from './DebugCHQueriesType' -export async function debugCHQueries(): Promise { +export function openCHQueriesDebugModal(): void { LemonDialog.open({ title: 'ClickHouse queries recently executed for this user', content: , diff --git a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx index 726864d585aa0..089fdb5f00b5e 100644 --- a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx +++ b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx @@ -49,7 +49,7 @@ import { DashboardType, InsightType } from '~/types' import { personalAPIKeysLogic } from '../../../scenes/settings/user/personalAPIKeysLogic' import type { commandPaletteLogicType } from './commandPaletteLogicType' -import { debugCHQueries } from './DebugCHQueries' +import { openCHQueriesDebugModal } from './DebugCHQueries' // If CommandExecutor returns CommandFlow, flow will be entered export type CommandExecutor = () => CommandFlow | void @@ -578,9 +578,7 @@ export const commandPaletteLogic = kea([ ? { icon: IconTools, display: 'Debug ClickHouse Queries', - executor: () => { - debugCHQueries() - }, + executor: () => openCHQueriesDebugModal(), } : [], } diff --git a/frontend/src/lib/components/CopyToClipboard.tsx b/frontend/src/lib/components/CopyToClipboard.tsx index c89895a0bae3a..a87358bfea496 100644 --- a/frontend/src/lib/components/CopyToClipboard.tsx +++ b/frontend/src/lib/components/CopyToClipboard.tsx @@ -29,8 +29,11 @@ export function CopyToClipboardInline({ style, ...props }: InlineProps): JSX.Element { - const copy = async (): Promise => - await copyToClipboard(explicitValue ?? (children ? children.toString() : ''), description) + if (typeof children !== 'string' && !explicitValue) { + throw new Error('CopyToClipboardInline must have a string child or explicitValue prop') + } + + const copy = async (): Promise => await copyToClipboard((explicitValue ?? children) as string, description) const content = ( setDateOption(newValue as string)} + onChange={(newValue): void => setDateOption(newValue)} onClick={(e): void => { e.stopPropagation() toggleDateOptionsSelector() diff --git a/frontend/src/lib/components/DateFilter/dateFilterLogic.ts b/frontend/src/lib/components/DateFilter/dateFilterLogic.ts index 00d25b04a7ae6..b077282409ab1 100644 --- a/frontend/src/lib/components/DateFilter/dateFilterLogic.ts +++ b/frontend/src/lib/components/DateFilter/dateFilterLogic.ts @@ -45,7 +45,7 @@ export const dateFilterLogic = kea([ }, ], rangeDateFrom: [ - (props.dateFrom && (dayjs.isDayjs(props.dateFrom) || isDate.test(props.dateFrom as string)) + (props.dateFrom && (dayjs.isDayjs(props.dateFrom) || isDate.test(props.dateFrom)) ? dayjs(props.dateFrom) : null) as Dayjs | null, { @@ -54,7 +54,7 @@ export const dateFilterLogic = kea([ }, ], rangeDateTo: [ - (props.dateTo && (dayjs.isDayjs(props.dateTo) || isDate.test(props.dateTo as string)) + (props.dateTo && (dayjs.isDayjs(props.dateTo) || isDate.test(props.dateTo)) ? dayjs(props.dateTo) : dayjs()) as Dayjs | null, { diff --git a/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.test.ts b/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.test.ts index 5a56fd3b739df..1db1f33132221 100644 --- a/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.test.ts +++ b/frontend/src/lib/components/DefinitionPopover/definitionPopoverLogic.test.ts @@ -66,8 +66,8 @@ describe('definitionPopoverLogic', () => { it('make local state dirty', async () => { await expectLogic(logic, async () => { - await logic.actions.setDefinition(mockEventDefinitions[0]) - await logic.actions.setPopoverState(DefinitionPopoverState.Edit) + logic.actions.setDefinition(mockEventDefinitions[0]) + logic.actions.setPopoverState(DefinitionPopoverState.Edit) }) .toDispatchActions(['setDefinition', 'setPopoverState']) .toMatchValues({ @@ -88,9 +88,9 @@ describe('definitionPopoverLogic', () => { it('cancel', async () => { await expectLogic(logic, async () => { - await logic.actions.setDefinition(mockEventDefinitions[0]) - await logic.actions.setPopoverState(DefinitionPopoverState.Edit) - await logic.actions.setLocalDefinition({ description: 'new description' }) + logic.actions.setDefinition(mockEventDefinitions[0]) + logic.actions.setPopoverState(DefinitionPopoverState.Edit) + logic.actions.setLocalDefinition({ description: 'new description' }) }) .toDispatchActions(['setLocalDefinition']) .toMatchValues({ @@ -160,7 +160,7 @@ describe('definitionPopoverLogic', () => { }, { type: TaxonomicFilterGroupType.Cohorts, - definition: mockCohort as CohortType, + definition: mockCohort, url: `api/projects/@current/cohorts/${mockCohort.id}`, dispatchActions: [cohortsModel, ['updateCohort']], }, @@ -179,10 +179,10 @@ describe('definitionPopoverLogic', () => { logic.mount() const expectChain = expectLogic(logic, async () => { - await logic.actions.setDefinition(group.definition) - await logic.actions.setPopoverState(DefinitionPopoverState.Edit) - await logic.actions.setLocalDefinition({ description: 'new and improved description' }) - await logic.actions.handleSave({}) + logic.actions.setDefinition(group.definition) + logic.actions.setPopoverState(DefinitionPopoverState.Edit) + logic.actions.setLocalDefinition({ description: 'new and improved description' }) + logic.actions.handleSave({}) }).toDispatchActions(['setDefinitionSuccess', 'setPopoverState', 'handleSave']) if (group.dispatchActions.length > 0) { @@ -203,9 +203,9 @@ describe('definitionPopoverLogic', () => { it('add tags', async () => { await expectLogic(logic, async () => { - await logic.actions.setDefinition(mockEventDefinitions[0]) - await logic.actions.setPopoverState(DefinitionPopoverState.Edit) - await logic.actions.setLocalDefinition({ tags: ['ohhello', 'ohwow'] }) + logic.actions.setDefinition(mockEventDefinitions[0]) + logic.actions.setPopoverState(DefinitionPopoverState.Edit) + logic.actions.setLocalDefinition({ tags: ['ohhello', 'ohwow'] }) }) .toDispatchActions(['setDefinitionSuccess', 'setLocalDefinition']) .toMatchValues({ @@ -222,8 +222,8 @@ describe('definitionPopoverLogic', () => { logic.mount() await expectLogic(logic, async () => { - await logic.actions.setDefinition(mockEventDefinitions[0]) - await logic.actions.setDefinition(mockEventDefinitions[1]) + logic.actions.setDefinition(mockEventDefinitions[0]) + logic.actions.setDefinition(mockEventDefinitions[1]) }) .toDispatchActions(['setDefinitionSuccess']) .toMatchValues({ diff --git a/frontend/src/lib/components/ExportButton/ExportButton.tsx b/frontend/src/lib/components/ExportButton/ExportButton.tsx index 71b5cd24969ea..6f2b40f8d0628 100644 --- a/frontend/src/lib/components/ExportButton/ExportButton.tsx +++ b/frontend/src/lib/components/ExportButton/ExportButton.tsx @@ -53,7 +53,7 @@ export function ExportButton({ items, ...buttonProps }: ExportButtonProps): JSX. key={i} fullWidth status="stealth" - onClick={() => triggerExport(triggerExportProps)} + onClick={() => void triggerExport(triggerExportProps)} data-attr={`export-button-${exportFormatExtension}`} data-ph-capture-attribute-export-target={target} data-ph-capture-attribute-export-body={ diff --git a/frontend/src/lib/components/ExportButton/exporter.tsx b/frontend/src/lib/components/ExportButton/exporter.tsx index 6fbe1fb75b81f..97cff3343e00c 100644 --- a/frontend/src/lib/components/ExportButton/exporter.tsx +++ b/frontend/src/lib/components/ExportButton/exporter.tsx @@ -49,8 +49,8 @@ export async function triggerExport(asset: TriggerExportProps): Promise { lemonToast.error('Export failed!') } } else { - // eslint-disable-next-line no-async-promise-executor - const poller = new Promise(async (resolve, reject) => { + // eslint-disable-next-line no-async-promise-executor,@typescript-eslint/no-misused-promises + const poller = new Promise(async (resolve, reject) => { const trackingProperties = { export_format: asset.export_format, dashboard: asset.dashboard, diff --git a/frontend/src/lib/components/FullScreen.tsx b/frontend/src/lib/components/FullScreen.tsx index 2ca3ec95706e0..95859d4efe88e 100644 --- a/frontend/src/lib/components/FullScreen.tsx +++ b/frontend/src/lib/components/FullScreen.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react' export function FullScreen({ onExit }: { onExit?: () => any }): null { const selector = 'aside.ant-layout-sider, .layout-top-content' useEffect(() => { - const myClasses = window.document.querySelectorAll(selector) as NodeListOf + const myClasses = window.document.querySelectorAll(selector) for (let i = 0; i < myClasses.length; i++) { myClasses[i].style.display = 'none' @@ -16,7 +16,7 @@ export function FullScreen({ onExit }: { onExit?: () => any }): null { } try { - window.document.body.requestFullscreen().then(() => { + void document.body.requestFullscreen().then(() => { window.addEventListener('fullscreenchange', handler, false) }) } catch { @@ -31,15 +31,15 @@ export function FullScreen({ onExit }: { onExit?: () => any }): null { } return () => { - const elements = window.document.querySelectorAll(selector) as NodeListOf + const elements = window.document.querySelectorAll(selector) for (let i = 0; i < elements.length; i++) { elements[i].style.display = 'block' } try { window.removeEventListener('fullscreenchange', handler, false) - if (window.document.fullscreenElement !== null) { - window.document.exitFullscreen() + if (document.fullscreenElement !== null) { + void document.exitFullscreen() } } catch { // will break on IE11 diff --git a/frontend/src/lib/components/ObjectTags/objectTagsLogic.test.ts b/frontend/src/lib/components/ObjectTags/objectTagsLogic.test.ts index e9430b0f28d55..4564a203301c0 100644 --- a/frontend/src/lib/components/ObjectTags/objectTagsLogic.test.ts +++ b/frontend/src/lib/components/ObjectTags/objectTagsLogic.test.ts @@ -29,7 +29,7 @@ describe('objectTagsLogic', () => { }) it('handle adding a new tag', async () => { await expectLogic(logic, async () => { - await logic.actions.setNewTag('Nigh') + logic.actions.setNewTag('Nigh') logic.actions.handleAdd('Nightly') }) .toDispatchActions(['setNewTag']) @@ -44,7 +44,7 @@ describe('objectTagsLogic', () => { newTag: '', }) // @ts-expect-error - const mockedOnChange = props.onChange?.mock as any + const mockedOnChange = props.onChange?.mock expect(mockedOnChange.calls.length).toBe(1) expect(mockedOnChange.calls[0][0]).toBe('nightly') expect(mockedOnChange.calls[0][1]).toEqual(['a', 'b', 'c', 'nightly']) @@ -70,7 +70,7 @@ describe('objectTagsLogic', () => { tags: ['b', 'c'], }) // @ts-expect-error - const mockedOnChange = props.onChange?.mock as any + const mockedOnChange = props.onChange?.mock expect(mockedOnChange.calls.length).toBe(1) expect(mockedOnChange.calls[0][0]).toBe('a') expect(mockedOnChange.calls[0][1]).toEqual(['b', 'c']) diff --git a/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx b/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx index 96365c27f98c3..59c9757f4ae73 100644 --- a/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx +++ b/frontend/src/lib/components/PropertiesTable/PropertiesTable.tsx @@ -285,7 +285,7 @@ export function PropertiesTable({ title: '', width: 0, render: function Copy(_, item: any): JSX.Element | false { - if (Array.isArray(item[1]) || item[1] instanceof Object) { + if (Array.isArray(item[1]) || item[1] instanceof Object || item[1] === null) { return false } return ( diff --git a/frontend/src/lib/components/PropertyFilters/utils.ts b/frontend/src/lib/components/PropertyFilters/utils.ts index 1bf0b20cd3bb7..e88596138c4d1 100644 --- a/frontend/src/lib/components/PropertyFilters/utils.ts +++ b/frontend/src/lib/components/PropertyFilters/utils.ts @@ -41,7 +41,7 @@ export function parseProperties( return input || [] } if (input && !Array.isArray(input) && isPropertyGroup(input)) { - return flattenPropertyGroup([], input as PropertyGroupFilter) + return flattenPropertyGroup([], input) } // Old style dict properties return Object.entries(input).map(([inputKey, value]) => { diff --git a/frontend/src/lib/components/Sharing/SharingModal.stories.tsx b/frontend/src/lib/components/Sharing/SharingModal.stories.tsx index 4d9a4539a4a94..2f5bd324ebc25 100644 --- a/frontend/src/lib/components/Sharing/SharingModal.stories.tsx +++ b/frontend/src/lib/components/Sharing/SharingModal.stories.tsx @@ -64,7 +64,7 @@ const Template = (args: Partial & { licensed?: boolean }): JS created_at: '2022-06-28T12:30:51.459746Z', enabled: true, access_token: '1AEQjQ2xNLGoiyI0UnNlLzOiBZWWMQ', - ...(req.body as any), + ...req.body, }, ] }, diff --git a/frontend/src/lib/components/Sharing/SharingModal.tsx b/frontend/src/lib/components/Sharing/SharingModal.tsx index 12a47409d58c9..26f0937f47561 100644 --- a/frontend/src/lib/components/Sharing/SharingModal.tsx +++ b/frontend/src/lib/components/Sharing/SharingModal.tsx @@ -114,7 +114,7 @@ export function SharingModalContent({ await copyToClipboard(shareLink, 'link')} + onClick={() => void copyToClipboard(shareLink, 'link')} icon={} > Copy public link diff --git a/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts b/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts index e8c323d0e41b5..fd57f9bbeae5c 100644 --- a/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts +++ b/frontend/src/lib/components/Subscriptions/subscriptionsLogic.ts @@ -47,8 +47,8 @@ export const subscriptionsLogic = kea([ }), listeners(({ actions }) => ({ - deleteSubscription: ({ id }) => { - deleteWithUndo({ + deleteSubscription: async ({ id }) => { + await deleteWithUndo({ endpoint: api.subscriptions.determineDeleteEndpoint(), object: { name: 'Subscription', id }, callback: () => actions.loadSubscriptions(), diff --git a/frontend/src/lib/hooks/useAsyncHandler.ts b/frontend/src/lib/hooks/useAsyncHandler.ts index 3f0f4717ea7d0..af962ad35a322 100644 --- a/frontend/src/lib/hooks/useAsyncHandler.ts +++ b/frontend/src/lib/hooks/useAsyncHandler.ts @@ -9,7 +9,7 @@ import { useState } from 'react' * return Click me */ export function useAsyncHandler( - onEvent: ((e: E) => any | Promise) | undefined + onEvent: ((e: E) => any) | undefined ): { loading: boolean; onEvent: ((e: E) => void) | undefined } { const [loading, setLoading] = useState(false) @@ -19,7 +19,7 @@ export function useAsyncHandler( const result = onEvent(e) if (result instanceof Promise) { setLoading(true) - result.finally(() => setLoading(false)) + void result.finally(() => setLoading(false)) } } } diff --git a/frontend/src/lib/internalMetrics.ts b/frontend/src/lib/internalMetrics.ts index b0ce237563e02..9ae89f02c235d 100644 --- a/frontend/src/lib/internalMetrics.ts +++ b/frontend/src/lib/internalMetrics.ts @@ -58,7 +58,7 @@ export async function apiGetWithTimeToSeeDataTracking( error = e } const requestDurationMs = performance.now() - requestStartMs - captureTimeToSeeData(teamId, { + void captureTimeToSeeData(teamId, { ...timeToSeeDataPayload, api_url: url, status: error ? 'failure' : 'success', diff --git a/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.scss b/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.scss index 43285996e4742..fd66747c15565 100644 --- a/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.scss +++ b/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.scss @@ -18,6 +18,10 @@ .LemonSegmentedButton__option { display: flex; flex: 1; + + .LemonButton__content { + text-wrap: nowrap; + } } } diff --git a/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.tsx b/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.tsx index 67766d42b532c..2e8e6ef391975 100644 --- a/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.tsx +++ b/frontend/src/lib/lemon-ui/LemonSegmentedButton/LemonSegmentedButton.tsx @@ -7,7 +7,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import React from 'react' import { useSliderPositioning } from '../hooks' -import { LemonButton } from '../LemonButton' +import { LemonButton, LemonButtonProps } from '../LemonButton' export interface LemonSegmentedButtonOption { value: T @@ -23,7 +23,7 @@ export interface LemonSegmentedButtonProps { value?: T onChange?: (newValue: T) => void options: LemonSegmentedButtonOption[] - size?: 'small' | 'medium' + size?: LemonButtonProps['size'] className?: string fullWidth?: boolean } diff --git a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx index acab59d22faf6..9c085799bb855 100644 --- a/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx +++ b/frontend/src/lib/lemon-ui/LemonSelectMultiple/LemonSelectMultiple.tsx @@ -89,7 +89,7 @@ export function LemonSelectMultiple({ const typedOnChange = onChange as (newValue: string | null) => void typedOnChange(typedValues) } else { - const typedValues = v.map((token) => token.toString().trim()) as string[] + const typedValues = v.map((token) => token.toString().trim()) const typedOnChange = onChange as (newValue: string[]) => void typedOnChange(typedValues) } diff --git a/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx b/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx index e2bbd1725443a..07fb82adffecd 100644 --- a/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx +++ b/frontend/src/lib/lemon-ui/ProfilePicture/ProfilePicture.tsx @@ -49,7 +49,7 @@ export function ProfilePicture({ const emailHash = md5(emailOrNameWithEmail.trim().toLowerCase()) const tentativeUrl = `https://www.gravatar.com/avatar/${emailHash}?s=96&d=404` // The image will be cached, so it's best to do GET request check before trying to render it - fetch(tentativeUrl).then((response) => { + void fetch(tentativeUrl).then((response) => { if (response.status === 200) { setGravatarUrl(tentativeUrl) } diff --git a/frontend/src/lib/lemon-ui/lemonToast.tsx b/frontend/src/lib/lemon-ui/lemonToast.tsx index 5c8c66c3c8ba7..5af288acc6c96 100644 --- a/frontend/src/lib/lemon-ui/lemonToast.tsx +++ b/frontend/src/lib/lemon-ui/lemonToast.tsx @@ -19,7 +19,7 @@ export function ToastCloseButton({ closeToast }: { closeToast?: () => void }): J interface ToastButton { label: string - action: () => void + action: (() => void) | (() => Promise) dataAttr?: string } @@ -48,7 +48,7 @@ export function ToastContent({ type, message, button, id }: ToastContentProps): {button && ( { - button.action() + void button.action() toast.dismiss(id) }} type="secondary" @@ -85,7 +85,7 @@ export const lemonToast = { }, warning(message: string | JSX.Element, { button, ...toastOptions }: ToastOptionsWithButton = {}): void { posthog.capture('toast warning', { - message: message.toString(), + message: String(message), button: button?.label, toastId: toastOptions.toastId, }) @@ -97,7 +97,7 @@ export const lemonToast = { }, error(message: string | JSX.Element, { button, ...toastOptions }: ToastOptionsWithButton = {}): void { posthog.capture('toast error', { - message: message.toString(), + message: String(message), button: button?.label, toastId: toastOptions.toastId, }) diff --git a/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.test.ts b/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.test.ts index 727b1a20077f2..e1a029a9d2db0 100644 --- a/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.test.ts +++ b/frontend/src/lib/logic/inAppPrompt/inAppPromptLogic.test.ts @@ -8,7 +8,7 @@ import { useMocks } from '~/mocks/jest' import { initKeaTests } from '~/test/init' import { inAppPromptEventCaptureLogic } from './inAppPromptEventCaptureLogic' -import { inAppPromptLogic, PromptConfig, PromptSequence, PromptUserState } from './inAppPromptLogic' +import { inAppPromptLogic, PromptConfig, PromptUserState } from './inAppPromptLogic' const configProductTours: PromptConfig & { state: PromptUserState } = { sequences: [ @@ -291,7 +291,7 @@ describe('inAppPromptLogic', () => { }) .toDispatchActions([ 'closePrompts', - logic.actionCreators.runSequence(configProductTours.sequences[1] as PromptSequence, 0), + logic.actionCreators.runSequence(configProductTours.sequences[1], 0), inAppPromptEventCaptureLogic.actionCreators.reportPromptShown( 'tooltip', configProductTours.sequences[1].key, @@ -335,7 +335,7 @@ describe('inAppPromptLogic', () => { logic.actions.nextPrompt() }) .toDispatchActions([ - logic.actionCreators.runSequence(configProductTours.sequences[1] as PromptSequence, 1), + logic.actionCreators.runSequence(configProductTours.sequences[1], 1), inAppPromptEventCaptureLogic.actionCreators.reportPromptForward( configProductTours.sequences[1].key, 1, @@ -361,7 +361,7 @@ describe('inAppPromptLogic', () => { logic.actions.previousPrompt() }) .toDispatchActions([ - logic.actionCreators.runSequence(configProductTours.sequences[1] as PromptSequence, 0), + logic.actionCreators.runSequence(configProductTours.sequences[1], 0), inAppPromptEventCaptureLogic.actionCreators.reportPromptBackward( configProductTours.sequences[1].key, 0, diff --git a/frontend/src/lib/taxonomy.tsx b/frontend/src/lib/taxonomy.tsx index a8ab8d6de3a7d..056a699c77742 100644 --- a/frontend/src/lib/taxonomy.tsx +++ b/frontend/src/lib/taxonomy.tsx @@ -836,7 +836,7 @@ export function getKeyMapping( data = { ...KEY_MAPPING[type][value.replace(/^\$initial_/, '$')] } if (data.description) { data.label = `Initial ${data.label}` - data.description = `${data.description} Data from the first time this user was seen.` + data.description = `${String(data.description)} Data from the first time this user was seen.` } return data } else if (value.startsWith('$survey_responded/')) { diff --git a/frontend/src/lib/utils.tsx b/frontend/src/lib/utils.tsx index 1240f07de14ba..80fd4009aed8b 100644 --- a/frontend/src/lib/utils.tsx +++ b/frontend/src/lib/utils.tsx @@ -1387,7 +1387,7 @@ export function humanTzOffset(timezone?: string): string { /** Join array of string into a list ("a, b, and c"). Uses the Oxford comma, but only if there are at least 3 items. */ export function humanList(arr: readonly string[]): string { - return arr.length > 2 ? arr.slice(0, -1).join(', ') + ', and ' + arr.slice(-1) : arr.join(' and ') + return arr.length > 2 ? arr.slice(0, -1).join(', ') + ', and ' + arr.at(-1) : arr.join(' and ') } export function resolveWebhookService(webhookUrl: string): string { @@ -1449,7 +1449,7 @@ export function lightenDarkenColor(hex: string, pct: number): string { return `rgb(${[r, g, b].join(',')})` } -export function toString(input?: any | null): string { +export function toString(input?: any): string { return input?.toString() || '' } diff --git a/frontend/src/lib/utils/eventUsageLogic.ts b/frontend/src/lib/utils/eventUsageLogic.ts index e13c61caaca84..e171c5829c359 100644 --- a/frontend/src/lib/utils/eventUsageLogic.ts +++ b/frontend/src/lib/utils/eventUsageLogic.ts @@ -6,7 +6,6 @@ import { now } from 'lib/dayjs' import { isPostHogProp, keyMappingKeys } from 'lib/taxonomy' import { convertPropertyGroupToProperties } from 'lib/utils' import posthog from 'posthog-js' -import { Framework, PlatformType } from 'scenes/ingestion/types' import { isFilterWithDisplay, isFunnelsFilter, @@ -334,7 +333,6 @@ export const eventUsageLogic = kea([ ) => ({ attribute, originalLength, newLength }), reportDashboardShareToggled: (isShared: boolean) => ({ isShared }), reportUpgradeModalShown: (featureName: string) => ({ featureName }), - reportIngestionLandingSeen: true, reportTimezoneComponentViewed: ( component: 'label' | 'indicator', project_timezone?: string, @@ -441,27 +439,11 @@ export const eventUsageLogic = kea([ reportInsightOpenedFromRecentInsightList: true, reportRecordingOpenedFromRecentRecordingList: true, reportPersonOpenedFromNewlySeenPersonsList: true, - reportIngestionSelectPlatformType: (platform: PlatformType) => ({ platform }), - reportIngestionSelectFrameworkType: (framework: Framework) => ({ framework }), - reportIngestionRecordingsTurnedOff: ( - session_recording_opt_in: boolean, - capture_console_log_opt_in: boolean, - capture_performance_opt_in: boolean - ) => ({ session_recording_opt_in, capture_console_log_opt_in, capture_performance_opt_in }), - reportIngestionAutocaptureToggled: (autocapture_opt_out: boolean) => ({ autocapture_opt_out }), - reportIngestionAutocaptureExceptionsToggled: (autocapture_opt_in: boolean) => ({ autocapture_opt_in }), - reportIngestionHelpClicked: (type: string) => ({ type }), - reportIngestionTryWithBookmarkletClicked: true, - reportIngestionTryWithDemoDataClicked: true, reportIngestionContinueWithoutVerifying: true, - reportIngestionContinueWithoutBilling: true, - reportIngestionBillingCancelled: true, - reportIngestionThirdPartyAboutClicked: (name: string) => ({ name }), - reportIngestionThirdPartyConfigureClicked: (name: string) => ({ name }), - reportIngestionThirdPartyPluginInstalled: (name: string) => ({ name }), + reportAutocaptureToggled: (autocapture_opt_out: boolean) => ({ autocapture_opt_out }), + reportAutocaptureExceptionsToggled: (autocapture_opt_in: boolean) => ({ autocapture_opt_in }), reportFailedToCreateFeatureFlagWithCohort: (code: string, detail: string) => ({ code, detail }), reportInviteMembersButtonClicked: true, - reportIngestionSidebarButtonClicked: (name: string) => ({ name }), reportDashboardLoadingTime: (loadingMilliseconds: number, dashboardId: number) => ({ loadingMilliseconds, dashboardId, @@ -796,9 +778,6 @@ export const eventUsageLogic = kea([ } posthog.capture('test account filters updated', payload) }, - reportIngestionLandingSeen: async () => { - posthog.capture('ingestion landing seen') - }, reportInsightFilterRemoved: async ({ index }) => { posthog.capture('local filter removed', { index }) @@ -1051,70 +1030,17 @@ export const eventUsageLogic = kea([ reportPersonOpenedFromNewlySeenPersonsList: () => { posthog.capture('person opened from newly seen persons list') }, - reportIngestionSelectPlatformType: ({ platform }) => { - posthog.capture('ingestion select platform type', { - platform: platform, - }) - }, - reportIngestionSelectFrameworkType: ({ framework }) => { - posthog.capture('ingestion select framework type', { - framework: framework, - }) - }, - reportIngestionRecordingsTurnedOff: ({ - session_recording_opt_in, - capture_console_log_opt_in, - capture_performance_opt_in, - }) => { - posthog.capture('ingestion recordings turned off', { - session_recording_opt_in, - capture_console_log_opt_in, - capture_performance_opt_in, - }) - }, - reportIngestionAutocaptureToggled: ({ autocapture_opt_out }) => { - posthog.capture('ingestion autocapture toggled', { - autocapture_opt_out, - }) - }, - reportIngestionAutocaptureExceptionsToggled: ({ autocapture_opt_in }) => { - posthog.capture('ingestion autocapture exceptions toggled', { - autocapture_opt_in, - }) - }, - reportIngestionHelpClicked: ({ type }) => { - posthog.capture('ingestion help clicked', { - type: type, - }) - }, - reportIngestionTryWithBookmarkletClicked: () => { - posthog.capture('ingestion try posthog with bookmarklet clicked') - }, - reportIngestionTryWithDemoDataClicked: () => { - posthog.capture('ingestion try posthog with demo data clicked') - }, reportIngestionContinueWithoutVerifying: () => { posthog.capture('ingestion continue without verifying') }, - reportIngestionContinueWithoutBilling: () => { - posthog.capture('ingestion continue without adding billing details') - }, - reportIngestionBillingCancelled: () => { - posthog.capture('ingestion billing cancelled') - }, - reportIngestionThirdPartyAboutClicked: ({ name }) => { - posthog.capture('ingestion third party about clicked', { - name: name, - }) - }, - reportIngestionThirdPartyConfigureClicked: ({ name }) => { - posthog.capture('ingestion third party configure clicked', { - name: name, + reportAutocaptureToggled: ({ autocapture_opt_out }) => { + posthog.capture('autocapture toggled', { + autocapture_opt_out, }) }, - reportIngestionThirdPartyPluginInstalled: ({ name }) => { - posthog.capture('report ingestion third party plugin installed', { - name: name, + reportAutocaptureExceptionsToggled: ({ autocapture_opt_in }) => { + posthog.capture('autocapture exceptions toggled', { + autocapture_opt_in, }) }, reportFailedToCreateFeatureFlagWithCohort: ({ detail, code }) => { @@ -1123,11 +1049,6 @@ export const eventUsageLogic = kea([ reportInviteMembersButtonClicked: () => { posthog.capture('invite members button clicked') }, - reportIngestionSidebarButtonClicked: ({ name }) => { - posthog.capture('ingestion sidebar button clicked', { - name: name, - }) - }, reportTeamSettingChange: ({ name, value }) => { posthog.capture(`${name} team setting updated`, { setting: name, diff --git a/frontend/src/models/cohortsModel.ts b/frontend/src/models/cohortsModel.ts index 83a9bef5362ce..9058bf3442361 100644 --- a/frontend/src/models/cohortsModel.ts +++ b/frontend/src/models/cohortsModel.ts @@ -105,8 +105,8 @@ export const cohortsModel = kea([ } await triggerExport(exportCommand) }, - deleteCohort: ({ cohort }) => { - deleteWithUndo({ + deleteCohort: async ({ cohort }) => { + await deleteWithUndo({ endpoint: api.cohorts.determineDeleteEndpoint(), object: cohort, callback: actions.loadCohorts, diff --git a/frontend/src/models/notebooksModel.ts b/frontend/src/models/notebooksModel.ts index 6d5e505986467..1d6dd14ce0e62 100644 --- a/frontend/src/models/notebooksModel.ts +++ b/frontend/src/models/notebooksModel.ts @@ -14,7 +14,7 @@ import { urls } from 'scenes/urls' import { filtersToQueryNode } from '~/queries/nodes/InsightQuery/utils/filtersToQueryNode' import { InsightVizNode, Node, NodeKind } from '~/queries/schema' -import { DashboardType, NotebookListItemType, NotebookNodeType, NotebookTarget, NotebookType } from '~/types' +import { DashboardType, NotebookListItemType, NotebookNodeType, NotebookTarget } from '~/types' import type { notebooksModelType } from './notebooksModelType' @@ -80,7 +80,7 @@ export const notebooksModel = kea([ reducers({ scratchpadNotebook: [ - SCRATCHPAD_NOTEBOOK as NotebookListItemType, + SCRATCHPAD_NOTEBOOK, { setScratchpadNotebook: (_, { notebook }) => notebook, }, @@ -105,7 +105,7 @@ export const notebooksModel = kea([ content: defaultNotebookContent(title, content), }) - openNotebook(notebook.short_id, location, 'end', (logic) => { + await openNotebook(notebook.short_id, location, 'end', (logic) => { onCreate?.(logic) }) @@ -117,7 +117,7 @@ export const notebooksModel = kea([ }, deleteNotebook: async ({ shortId, title }) => { - deleteWithUndo({ + await deleteWithUndo({ endpoint: `projects/${values.currentTeamId}/notebooks`, object: { name: title || shortId, id: shortId }, callback: actions.loadNotebooks, @@ -137,14 +137,14 @@ export const notebooksModel = kea([ }, ], notebookTemplates: [ - LOCAL_NOTEBOOK_TEMPLATES as NotebookType[], + LOCAL_NOTEBOOK_TEMPLATES, { // In the future we can load these from remote }, ], })), - listeners(({ actions }) => ({ + listeners(({ asyncActions }) => ({ createNotebookFromDashboard: async ({ dashboard }) => { const queries = dashboard.tiles.reduce((acc, tile) => { if (!tile.insight) { @@ -185,7 +185,7 @@ export const notebooksModel = kea([ }, })) - await actions.createNotebook(NotebookTarget.Scene, dashboard.name + ' (copied)', resources) + await asyncActions.createNotebook(NotebookTarget.Scene, dashboard.name + ' (copied)', resources) }, })), ]) diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 5fa9dd585652e..b814b6112fb13 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -471,7 +471,7 @@ export function DataTable({ uniqueKey, query, setQuery, context, cachedResults } '::' ) /* Bust the LemonTable cache when columns change */ } - dataSource={(dataTableRows ?? []) as DataTableRow[]} + dataSource={dataTableRows ?? []} rowKey={({ result }: DataTableRow, rowIndex) => { if (result) { if ( diff --git a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx index b2f7edcd837a5..82c090b6eeed5 100644 --- a/frontend/src/queries/nodes/DataTable/DataTableExport.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTableExport.tsx @@ -3,6 +3,7 @@ import { useValues } from 'kea' import { triggerExport } from 'lib/components/ExportButton/exporter' import { IconExport } from 'lib/lemon-ui/icons' import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' +import { copyToClipboard } from 'lib/utils' import Papa from 'papaparse' import { asDisplay } from 'scenes/persons/person-utils' import { urls } from 'scenes/urls' @@ -22,7 +23,7 @@ import { dataTableLogic, DataTableRow } from './dataTableLogic' const EXPORT_MAX_LIMIT = 10000 -function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void { +async function startDownload(query: DataTableNode, onlySelectedColumns: boolean): Promise { const exportContext = isPersonsNode(query.source) ? { path: getPersonsEndpoint(query.source) } : { source: query.source } @@ -47,7 +48,7 @@ function startDownload(query: DataTableNode, onlySelectedColumns: boolean): void ) } } - triggerExport({ + await triggerExport({ export_format: ExporterFormat.CSV, export_context: exportContext, }) @@ -158,9 +159,7 @@ function copyTableToCsv(dataTableRows: DataTableRow[], columns: string[], query: const csv = Papa.unparse(tableData) - navigator.clipboard.writeText(csv).then(() => { - lemonToast.success('Table copied to clipboard!') - }) + void copyToClipboard(csv, 'table') } catch { lemonToast.error('Copy failed!') } @@ -172,9 +171,7 @@ function copyTableToJson(dataTableRows: DataTableRow[], columns: string[], query const json = JSON.stringify(tableData, null, 4) - navigator.clipboard.writeText(json).then(() => { - lemonToast.success('Table copied to clipboard!') - }) + void copyToClipboard(json, 'table') } catch { lemonToast.error('Copy failed!') } @@ -206,7 +203,7 @@ export function DataTableExport({ query }: DataTableExportProps): JSX.Element | key={1} placement={'topRight'} onConfirm={() => { - startDownload(query, true) + void startDownload(query, true) }} actor={isPersonsNode(query.source) ? 'persons' : 'events'} limit={EXPORT_MAX_LIMIT} @@ -222,7 +219,7 @@ export function DataTableExport({ query }: DataTableExportProps): JSX.Element | startDownload(query, false)} + onConfirm={() => void startDownload(query, false)} actor={isPersonsNode(query.source) ? 'persons' : 'events'} limit={EXPORT_MAX_LIMIT} > diff --git a/frontend/src/queries/nodes/DataTable/EventRowActions.tsx b/frontend/src/queries/nodes/DataTable/EventRowActions.tsx index 6d8aafa4fb710..b0a4e35e7f4ce 100644 --- a/frontend/src/queries/nodes/DataTable/EventRowActions.tsx +++ b/frontend/src/queries/nodes/DataTable/EventRowActions.tsx @@ -28,7 +28,7 @@ export function EventRowActions({ event }: EventActionProps): JSX.Element { - createActionFromEvent( + void createActionFromEvent( getCurrentTeamId(), event, 0, @@ -47,8 +47,8 @@ export function EventRowActions({ event }: EventActionProps): JSX.Element { fullWidth sideIcon={} data-attr="events-table-event-link" - onClick={async () => - await copyToClipboard( + onClick={() => + void copyToClipboard( `${window.location.origin}${urls.event(String(event.uuid), event.timestamp)}`, 'link to event' ) diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 8371e9dca0977..264f7145e50e8 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -112,7 +112,7 @@ export const dataTableLogic = kea([ // Add a label between results if the day changed if (orderKey === 'timestamp' && orderKeyIndex !== -1) { - let lastResult: any | null = null + let lastResult: any = null const newResults: DataTableRow[] = [] for (const result of results) { if ( diff --git a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts index 2e11924e37aac..f64615df762e2 100644 --- a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts +++ b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts @@ -85,7 +85,7 @@ export const hogQLQueryEditorLogic = kea([ ], aiAvailable: [() => [preflightLogic.selectors.preflight], (preflight) => preflight?.openai_available], }), - listeners(({ actions, props, values }) => ({ + listeners(({ actions, asyncActions, props, values }) => ({ saveQuery: () => { const query = values.queryInput // TODO: Is below line necessary if the only way for queryInput to change is already through setQueryInput? @@ -181,7 +181,7 @@ export const hogQLQueryEditorLogic = kea([ kind: NodeKind.HogQLQuery, query: values.queryInput, } - await actions.createDataWarehouseSavedQuery({ name, query }) + await asyncActions.createDataWarehouseSavedQuery({ name, query }) }, })), ]) diff --git a/frontend/src/scenes/App.tsx b/frontend/src/scenes/App.tsx index 4e35242c7889a..fcab4997de5f0 100644 --- a/frontend/src/scenes/App.tsx +++ b/frontend/src/scenes/App.tsx @@ -167,9 +167,7 @@ function AppScene(): JSX.Element | null { return ( <> - - {wrappedSceneElement} - + {wrappedSceneElement} {toastContainer} diff --git a/frontend/src/scenes/PreflightCheck/preflightLogic.tsx b/frontend/src/scenes/PreflightCheck/preflightLogic.tsx index 33b9f168005a1..c57b8a20178b8 100644 --- a/frontend/src/scenes/PreflightCheck/preflightLogic.tsx +++ b/frontend/src/scenes/PreflightCheck/preflightLogic.tsx @@ -39,7 +39,7 @@ export const preflightLogic = kea([ null as PreflightStatus | null, { loadPreflight: async () => { - const response = (await api.get('_preflight/')) as PreflightStatus + const response = await api.get('_preflight/') return response }, }, diff --git a/frontend/src/scenes/actions/ActionStep.tsx b/frontend/src/scenes/actions/ActionStep.tsx index d93a1c4a0f60c..3bab146512df3 100644 --- a/frontend/src/scenes/actions/ActionStep.tsx +++ b/frontend/src/scenes/actions/ActionStep.tsx @@ -1,7 +1,6 @@ import './ActionStep.scss' -import { LemonButton, LemonInput, Link } from '@posthog/lemon-ui' -import { Radio, RadioChangeEvent } from 'antd' +import { LemonButton, LemonInput, LemonSegmentedButton, Link } from '@posthog/lemon-ui' import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' import { AuthorizedUrlListType } from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic' import { OperandTag } from 'lib/components/PropertyFilters/components/OperandTag' @@ -168,9 +167,10 @@ function Option({ return (
- - {label} {extra_options} - +
+ {label} + {extra_options} +
{caption &&
{caption}
} void }): JSX.Element { - const handleChange = (e: RadioChangeEvent): void => { - const type = e.target.value + const handleChange = (type: string): void => { if (type === '$autocapture') { sendStep({ ...step, event: '$autocapture' }) } else if (type === 'event') { @@ -303,19 +302,30 @@ function TypeSwitcher({ return (
- - Autocapture - Custom event - Page view - + options={[ + { + value: '$autocapture', + label: 'Autocapture', + }, + { + value: 'event', + label: 'Custom event', + }, + { + value: '$pageview', + label: 'Page view', + }, + ]} + fullWidth + size="small" + />
) } @@ -330,15 +340,32 @@ function StringMatchingSelection({ sendStep: (stepToSend: ActionStepType) => void }): JSX.Element { const key = `${field}_matching` - const handleURLMatchChange = (e: RadioChangeEvent): void => { - sendStep({ ...step, [key]: e.target.value }) + const handleURLMatchChange = (value: string): void => { + sendStep({ ...step, [key]: value }) } const defaultValue = field === 'url' ? StringMatching.Contains : StringMatching.Exact return ( - - matches exactly - matches regex - contains - +
+ +
) } diff --git a/frontend/src/scenes/actions/actionEditLogic.tsx b/frontend/src/scenes/actions/actionEditLogic.tsx index 985748c27e99f..a5b1e45e3f617 100644 --- a/frontend/src/scenes/actions/actionEditLogic.tsx +++ b/frontend/src/scenes/actions/actionEditLogic.tsx @@ -131,8 +131,8 @@ export const actionEditLogic = kea([ })), listeners(({ values, actions }) => ({ - deleteAction: () => { - deleteWithUndo({ + deleteAction: async () => { + await deleteWithUndo({ endpoint: api.actions.determineDeleteEndpoint(), object: values.action, callback: () => { diff --git a/frontend/src/scenes/actions/actionsLogic.ts b/frontend/src/scenes/actions/actionsLogic.ts index f3725765af47e..0d594d8ee2962 100644 --- a/frontend/src/scenes/actions/actionsLogic.ts +++ b/frontend/src/scenes/actions/actionsLogic.ts @@ -11,6 +11,8 @@ import { ActionType, Breadcrumb, ProductKey } from '~/types' import type { actionsLogicType } from './actionsLogicType' +export type ActionsFilterType = 'all' | 'me' + export const actionsFuse = new Fuse([], { keys: [{ name: 'name', weight: 2 }, 'description', 'tags'], threshold: 0.3, @@ -31,15 +33,15 @@ export const actionsLogic = kea([ ], })), actions({ - setFilterByMe: (filterByMe: boolean) => ({ filterByMe }), + setFilterType: (filterType: ActionsFilterType) => ({ filterType }), setSearchTerm: (searchTerm: string) => ({ searchTerm }), }), reducers({ - filterByMe: [ - false, + filterType: [ + 'all' as ActionsFilterType, { persist: true }, { - setFilterByMe: (_, { filterByMe }) => filterByMe, + setFilterType: (_, { filterType }) => filterType, }, ], searchTerm: [ @@ -51,13 +53,13 @@ export const actionsLogic = kea([ }), selectors({ actionsFiltered: [ - (s) => [s.actions, s.filterByMe, s.searchTerm, s.user], - (actions, filterByMe, searchTerm, user) => { + (s) => [s.actions, s.filterType, s.searchTerm, s.user], + (actions, filterType, searchTerm, user) => { let data = actions if (searchTerm) { data = actionsFuse.search(searchTerm).map((result) => result.item) } - if (filterByMe) { + if (filterType === 'me') { data = data.filter((item) => item.created_by?.uuid === user?.uuid) } return data diff --git a/frontend/src/scenes/appContextLogic.ts b/frontend/src/scenes/appContextLogic.ts index 38d923e7e538a..ce3a6a11a317c 100644 --- a/frontend/src/scenes/appContextLogic.ts +++ b/frontend/src/scenes/appContextLogic.ts @@ -27,7 +27,7 @@ export const appContextLogic = kea([ const preloadedUser = appContext?.current_user if (appContext && preloadedUser) { - api.get('api/users/@me/').then((remoteUser: UserType) => { + void api.get('api/users/@me/').then((remoteUser: UserType) => { if (remoteUser.uuid !== preloadedUser.uuid) { console.error(`Preloaded user ${preloadedUser.uuid} does not match remote user ${remoteUser.uuid}`) Sentry.captureException( diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index de98be24a7920..8c7a8c5ab8c09 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -23,6 +23,7 @@ export const appScenes: Record any> = { [Scene.PersonsManagement]: () => import('./persons-management/PersonsManagementScene'), [Scene.Person]: () => import('./persons/PersonScene'), [Scene.Pipeline]: () => import('./pipeline/Pipeline'), + [Scene.PipelineApp]: () => import('./pipeline/PipelineApp'), [Scene.Group]: () => import('./groups/Group'), [Scene.Action]: () => import('./actions/Action'), [Scene.Experiments]: () => import('./experiments/Experiments'), @@ -52,7 +53,6 @@ export const appScenes: Record any> = { [Scene.PreflightCheck]: () => import('./PreflightCheck/PreflightCheck'), [Scene.Signup]: () => import('./authentication/signup/SignupContainer'), [Scene.InviteSignup]: () => import('./authentication/InviteSignup'), - [Scene.Ingestion]: () => import('./ingestion/IngestionWizard'), [Scene.Billing]: () => import('./billing/Billing'), [Scene.Apps]: () => import('./plugins/AppsScene'), [Scene.FrontendAppScene]: () => import('./apps/FrontendAppScene'), diff --git a/frontend/src/scenes/apps/AppMetricsScene.tsx b/frontend/src/scenes/apps/AppMetricsScene.tsx index 46aeffa978557..78ff0d9fc7761 100644 --- a/frontend/src/scenes/apps/AppMetricsScene.tsx +++ b/frontend/src/scenes/apps/AppMetricsScene.tsx @@ -81,6 +81,11 @@ export function AppMetrics(): JSX.Element { label: <>onEvent metrics, content: , }, + showTab(AppMetricsTab.ComposeWebhook) && { + key: AppMetricsTab.ComposeWebhook, + label: <>composeWebhook metrics, + content: , + }, showTab(AppMetricsTab.ExportEvents) && { key: AppMetricsTab.ExportEvents, label: <>exportEvents metrics, diff --git a/frontend/src/scenes/apps/MetricsTab.tsx b/frontend/src/scenes/apps/MetricsTab.tsx index 5c6be447da71d..6ca189e0699b8 100644 --- a/frontend/src/scenes/apps/MetricsTab.tsx +++ b/frontend/src/scenes/apps/MetricsTab.tsx @@ -40,7 +40,7 @@ export function MetricsTab({ tab }: MetricsTabProps): JSX.Element { setDateFrom(newValue as string)} + onChange={(newValue) => setDateFrom(newValue)} options={[ { label: 'Last 30 days', value: '-30d' }, { label: 'Last 7 days', value: '-7d' }, diff --git a/frontend/src/scenes/apps/appMetricsSceneLogic.ts b/frontend/src/scenes/apps/appMetricsSceneLogic.ts index 3cd3e927c890a..ca6c7f3a08960 100644 --- a/frontend/src/scenes/apps/appMetricsSceneLogic.ts +++ b/frontend/src/scenes/apps/appMetricsSceneLogic.ts @@ -29,6 +29,7 @@ export enum AppMetricsTab { Logs = 'logs', ProcessEvent = 'processEvent', OnEvent = 'onEvent', + ComposeWebhook = 'composeWebhook', ExportEvents = 'exportEvents', ScheduledTask = 'scheduledTask', HistoricalExports = 'historical_exports', @@ -37,6 +38,7 @@ export enum AppMetricsTab { export const TabsWithMetrics = [ AppMetricsTab.ProcessEvent, AppMetricsTab.OnEvent, + AppMetricsTab.ComposeWebhook, AppMetricsTab.ExportEvents, AppMetricsTab.ScheduledTask, AppMetricsTab.HistoricalExports, @@ -96,6 +98,7 @@ const DEFAULT_DATE_FROM = '-30d' const INITIAL_TABS: Array = [ AppMetricsTab.ProcessEvent, AppMetricsTab.OnEvent, + AppMetricsTab.ComposeWebhook, AppMetricsTab.ExportEvents, AppMetricsTab.ScheduledTask, ] diff --git a/frontend/src/scenes/apps/constants.tsx b/frontend/src/scenes/apps/constants.tsx index 4d89a8202eae1..9870936b5cfc5 100644 --- a/frontend/src/scenes/apps/constants.tsx +++ b/frontend/src/scenes/apps/constants.tsx @@ -47,6 +47,26 @@ export const DescriptionColumns: Record = { ), }, + [AppMetricsTab.ComposeWebhook]: { + successes: 'Events processed', + successes_tooltip: ( + <> + These events were successfully processed by the composeWebhook app method on the first try. + + ), + successes_on_retry: 'Events processed on retry', + successes_on_retry_tooltip: ( + <> + These events were successfully processed by the composeWebhook app method after being retried. + + ), + failures: 'Failed events', + failures_tooltip: ( + <> + These events had errors when being processed by the composeWebhook app method. + + ), + }, [AppMetricsTab.ExportEvents]: { successes: 'Events delivered', successes_tooltip: ( diff --git a/frontend/src/scenes/authentication/inviteSignupLogic.ts b/frontend/src/scenes/authentication/inviteSignupLogic.ts index 590b2d819bdda..4950cac2750ca 100644 --- a/frontend/src/scenes/authentication/inviteSignupLogic.ts +++ b/frontend/src/scenes/authentication/inviteSignupLogic.ts @@ -90,7 +90,7 @@ export const inviteSignupLogic = kea([ first_name: !first_name ? 'Please enter your name' : undefined, }), submit: async (payload, breakpoint) => { - await breakpoint() + breakpoint() if (!values.invite) { return diff --git a/frontend/src/scenes/authentication/login2FALogic.ts b/frontend/src/scenes/authentication/login2FALogic.ts index da8fe2c8c4b12..4da5deb50adbe 100644 --- a/frontend/src/scenes/authentication/login2FALogic.ts +++ b/frontend/src/scenes/authentication/login2FALogic.ts @@ -67,7 +67,7 @@ export const login2FALogic = kea([ : null, }), submit: async ({ token }, breakpoint) => { - await breakpoint() + breakpoint() try { return await api.create('api/login/token', { token }) } catch (e) { diff --git a/frontend/src/scenes/authentication/loginLogic.ts b/frontend/src/scenes/authentication/loginLogic.ts index a31220ab578d4..419f04e3c91d6 100644 --- a/frontend/src/scenes/authentication/loginLogic.ts +++ b/frontend/src/scenes/authentication/loginLogic.ts @@ -84,7 +84,7 @@ export const loginLogic = kea([ return { status: 'pending' } } - await breakpoint() + breakpoint() const response = await api.create('api/login/precheck', { email }) return { status: 'completed', ...response } }, @@ -103,7 +103,7 @@ export const loginLogic = kea([ : undefined, }), submit: async ({ email, password }, breakpoint) => { - await breakpoint() + breakpoint() try { return await api.create('api/login', { email, password }) } catch (e) { diff --git a/frontend/src/scenes/authentication/passwordResetLogic.ts b/frontend/src/scenes/authentication/passwordResetLogic.ts index 21bd298372661..487510514e474 100644 --- a/frontend/src/scenes/authentication/passwordResetLogic.ts +++ b/frontend/src/scenes/authentication/passwordResetLogic.ts @@ -64,7 +64,7 @@ export const passwordResetLogic = kea([ email: !email ? 'Please enter your email to continue' : undefined, }), submit: async ({ email }, breakpoint) => { - await breakpoint() + breakpoint() try { await api.create('api/reset/', { email }) diff --git a/frontend/src/scenes/authentication/setup2FALogic.ts b/frontend/src/scenes/authentication/setup2FALogic.ts index d034a763aa398..7fe843a95fdaf 100644 --- a/frontend/src/scenes/authentication/setup2FALogic.ts +++ b/frontend/src/scenes/authentication/setup2FALogic.ts @@ -42,7 +42,7 @@ export const setup2FALogic = kea([ {}, { setup: async (_, breakpoint) => { - await breakpoint() + breakpoint() await api.get('api/users/@me/start_2fa_setup/') return { status: 'completed' } }, @@ -56,7 +56,7 @@ export const setup2FALogic = kea([ token: !token ? 'Please enter a token to continue' : undefined, }), submit: async ({ token }, breakpoint) => { - await breakpoint() + breakpoint() try { return await api.create('api/users/@me/validate_2fa/', { token }) } catch (e) { diff --git a/frontend/src/scenes/authentication/signup/signupForm/signupLogic.ts b/frontend/src/scenes/authentication/signup/signupForm/signupLogic.ts index 653ad4d7dfda4..803f47ecad608 100644 --- a/frontend/src/scenes/authentication/signup/signupForm/signupLogic.ts +++ b/frontend/src/scenes/authentication/signup/signupForm/signupLogic.ts @@ -88,7 +88,7 @@ export const signupLogic = kea([ organization_name: !organization_name ? 'Please enter your organization name' : undefined, }), submit: async (payload, breakpoint) => { - await breakpoint() + breakpoint() try { const res = await api.create('api/signup/', { ...values.signupPanel1, ...payload }) location.href = res.redirect_url || '/' diff --git a/frontend/src/scenes/batch_exports/batchExportEditLogic.ts b/frontend/src/scenes/batch_exports/batchExportEditLogic.ts index 406dadf283382..71ffad29af541 100644 --- a/frontend/src/scenes/batch_exports/batchExportEditLogic.ts +++ b/frontend/src/scenes/batch_exports/batchExportEditLogic.ts @@ -20,7 +20,7 @@ import type { batchExportsEditLogicType } from './batchExportEditLogicType' import { batchExportLogic } from './batchExportLogic' export type BatchExportsEditLogicProps = { - id: string | 'new' + id: string } export type BatchExportConfigurationForm = Omit< diff --git a/frontend/src/scenes/batch_exports/utils.ts b/frontend/src/scenes/batch_exports/utils.ts index 16ebf9d3176ef..56c07e26a7ccf 100644 --- a/frontend/src/scenes/batch_exports/utils.ts +++ b/frontend/src/scenes/batch_exports/utils.ts @@ -25,6 +25,10 @@ export function humanizeDestination(destination: BatchExportDestination): string return `postgresql://${destination.config.user}:***@${destination.config.host}:${destination.config.port}/${destination.config.database}` } + if (destination.type === 'Redshift') { + return `redshift://${destination.config.user}:***@${destination.config.host}:${destination.config.port}/${destination.config.database}` + } + if (destination.type === 'BigQuery') { return `bigquery:${destination.config.project_id}:${destination.config.dataset_id}:${destination.config.table_id}` } diff --git a/frontend/src/scenes/billing/PlanTable.tsx b/frontend/src/scenes/billing/PlanTable.tsx index 1e5421f63542e..30c244fbc3fdc 100644 --- a/frontend/src/scenes/billing/PlanTable.tsx +++ b/frontend/src/scenes/billing/PlanTable.tsx @@ -204,7 +204,9 @@ export function PlanTable({ redirectPath }: { redirectPath: string }): JSX.Eleme

{plans?.map((plan) => ( - {getProductTiers(plan, product.type)} + + {getProductTiers(plan, product.type)} + ))} )) diff --git a/frontend/src/scenes/billing/billingLogic.ts b/frontend/src/scenes/billing/billingLogic.ts index f27f7e11d6b76..5b3d8a61da1b8 100644 --- a/frontend/src/scenes/billing/billingLogic.ts +++ b/frontend/src/scenes/billing/billingLogic.ts @@ -10,7 +10,6 @@ import { pluralize } from 'lib/utils' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import posthog from 'posthog-js' import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { urls } from 'scenes/urls' import { userLogic } from 'scenes/userLogic' import { BillingProductV2Type, BillingV2Type } from '~/types' @@ -79,9 +78,7 @@ export const billingLogic = kea([ '' as string, { setRedirectPath: () => { - return window.location.pathname.includes('/ingestion') - ? urls.ingestion() + '/billing' - : window.location.pathname.includes('/onboarding') + return window.location.pathname.includes('/onboarding') ? window.location.pathname + window.location.search : '' }, @@ -90,7 +87,7 @@ export const billingLogic = kea([ isOnboarding: [ false, { - setIsOnboarding: () => window.location.pathname.includes('/ingestion'), + setIsOnboarding: () => window.location.pathname.includes('/onboarding'), }, ], }), diff --git a/frontend/src/scenes/cohorts/cohortEditLogic.test.ts b/frontend/src/scenes/cohorts/cohortEditLogic.test.ts index 02456a3affe55..94f6910e1b99b 100644 --- a/frontend/src/scenes/cohorts/cohortEditLogic.test.ts +++ b/frontend/src/scenes/cohorts/cohortEditLogic.test.ts @@ -78,8 +78,8 @@ describe('cohortEditLogic', () => { it('delete cohort', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort(mockCohort) - await logic.actions.deleteCohort() + logic.actions.setCohort(mockCohort) + logic.actions.deleteCohort() }) .toFinishAllListeners() .toDispatchActions(['setCohort', 'deleteCohort', router.actionCreators.push(urls.cohorts())]) @@ -93,7 +93,7 @@ describe('cohortEditLogic', () => { it('save with valid cohort', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -117,7 +117,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }).toDispatchActions(['setCohort', 'submitCohort', 'submitCohortSuccess']) expect(api.update).toBeCalledTimes(1) }) @@ -125,11 +125,11 @@ describe('cohortEditLogic', () => { it('do not save with invalid name', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, name: '', }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }).toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) expect(api.update).toBeCalledTimes(0) }) @@ -138,7 +138,7 @@ describe('cohortEditLogic', () => { it('do not save on OR operator', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -172,7 +172,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -200,7 +200,7 @@ describe('cohortEditLogic', () => { it('do not save on less than one positive matching criteria', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -226,7 +226,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -253,7 +253,7 @@ describe('cohortEditLogic', () => { it('do not save on criteria cancelling each other out', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -287,7 +287,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -318,7 +318,7 @@ describe('cohortEditLogic', () => { it('do not save on invalid lower and upper bound period values - perform event regularly', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -347,7 +347,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -377,7 +377,7 @@ describe('cohortEditLogic', () => { it('do not save on invalid lower and upper bound period values - perform events in sequence', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -404,7 +404,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -436,7 +436,7 @@ describe('cohortEditLogic', () => { it(`${key} row missing all required fields`, async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, filters: { properties: { @@ -462,7 +462,7 @@ describe('cohortEditLogic', () => { }, }, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }) .toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) .toMatchValues({ @@ -497,13 +497,13 @@ describe('cohortEditLogic', () => { it('can save existing static cohort with empty csv', async () => { await initCohortLogic({ id: 1 }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, is_static: true, groups: [], csv: undefined, }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }).toDispatchActions(['setCohort', 'submitCohort', 'submitCohortSuccess']) expect(api.update).toBeCalledTimes(1) }) @@ -511,14 +511,14 @@ describe('cohortEditLogic', () => { it('do not save static cohort with empty csv', async () => { await initCohortLogic({ id: 'new' }) await expectLogic(logic, async () => { - await logic.actions.setCohort({ + logic.actions.setCohort({ ...mockCohort, is_static: true, groups: [], csv: undefined, id: 'new', }) - await logic.actions.submitCohort() + logic.actions.submitCohort() }).toDispatchActions(['setCohort', 'submitCohort', 'submitCohortFailure']) expect(api.update).toBeCalledTimes(0) }) diff --git a/frontend/src/scenes/cohorts/cohortEditLogic.ts b/frontend/src/scenes/cohorts/cohortEditLogic.ts index f2abda9e2c6d2..2192995feca8d 100644 --- a/frontend/src/scenes/cohorts/cohortEditLogic.ts +++ b/frontend/src/scenes/cohorts/cohortEditLogic.ts @@ -76,7 +76,7 @@ export const cohortEditLogic = kea([ reducers(({ props, selectors }) => ({ cohort: [ - NEW_COHORT as CohortType, + NEW_COHORT, { setOuterGroupsType: (state, { type }) => ({ ...state, @@ -214,7 +214,7 @@ export const cohortEditLogic = kea([ loaders(({ actions, values, key }) => ({ cohort: [ - NEW_COHORT as CohortType, + NEW_COHORT, { setCohort: ({ cohort }) => processCohort(cohort), fetchCohort: async ({ id }, breakpoint) => { @@ -321,6 +321,7 @@ export const cohortEditLogic = kea([ checkIfFinishedCalculating: async ({ cohort }, breakpoint) => { if (cohort.is_calculating) { actions.setPollTimeout( + // eslint-disable-next-line @typescript-eslint/no-misused-promises window.setTimeout(async () => { const newCohort = await api.cohorts.get(cohort.id) breakpoint() diff --git a/frontend/src/scenes/dashboard/dashboardLogic.test.ts b/frontend/src/scenes/dashboard/dashboardLogic.test.ts index 81f004f937323..baf761f3e98f9 100644 --- a/frontend/src/scenes/dashboard/dashboardLogic.test.ts +++ b/frontend/src/scenes/dashboard/dashboardLogic.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ // let tiles assert an insight is present in tests i.e. `tile!.insight` when it must be present for tests to pass import { expectLogic, truth } from 'kea-test-utils' import api from 'lib/api' @@ -35,9 +34,9 @@ export function insightOnDashboard( insight: Partial = {} ): InsightModel { const tiles = dashboardJson.tiles.filter((tile) => !!tile.insight && tile.insight?.id === insightId) - let tile = dashboardJson.tiles[0] as DashboardTile + let tile = dashboardJson.tiles[0] if (tiles.length) { - tile = tiles[0] as DashboardTile + tile = tiles[0] } if (!tile.insight) { throw new Error('tile has no insight') @@ -220,7 +219,7 @@ describe('dashboardLogic', () => { const fromIndex = from.tiles.findIndex( (tile) => !!tile.insight && tile.insight.id === tileToUpdate.insight.id ) - const removedTile = from.tiles.splice(fromIndex, 1)[0] as DashboardTile + const removedTile = from.tiles.splice(fromIndex, 1)[0] // update the insight const insightId = tileToUpdate.insight.id @@ -356,7 +355,7 @@ describe('dashboardLogic', () => { const startingDashboard = dashboards['9'] const tiles = startingDashboard.tiles - const sourceTile = tiles[0] as DashboardTile + const sourceTile = tiles[0] await expectLogic(logic) .toFinishAllListeners() @@ -532,11 +531,11 @@ describe('dashboardLogic', () => { ]) .toMatchValues({ refreshStatus: { - [(dashboards['5'].tiles[0] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[0].insight!.short_id]: { loading: true, timer: expect.anything(), }, - [(dashboards['5'].tiles[1] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[1].insight!.short_id]: { loading: true, timer: expect.anything(), }, @@ -550,29 +549,21 @@ describe('dashboardLogic', () => { // and updates the action in the model (a) => a.type === dashboardsModel.actionTypes.updateDashboardInsight && - a.payload.insight.short_id === - (dashboards['5'].tiles[1] as DashboardTile).insight!.short_id, + a.payload.insight.short_id === dashboards['5'].tiles[1].insight!.short_id, (a) => a.type === dashboardsModel.actionTypes.updateDashboardInsight && - a.payload.insight.short_id === - (dashboards['5'].tiles[0] as DashboardTile).insight!.short_id, + a.payload.insight.short_id === dashboards['5'].tiles[0].insight!.short_id, // no longer reloading - logic.actionCreators.setRefreshStatus( - (dashboards['5'].tiles[0] as DashboardTile).insight!.short_id, - false - ), - logic.actionCreators.setRefreshStatus( - (dashboards['5'].tiles[1] as DashboardTile).insight!.short_id, - false - ), + logic.actionCreators.setRefreshStatus(dashboards['5'].tiles[0].insight!.short_id, false), + logic.actionCreators.setRefreshStatus(dashboards['5'].tiles[1].insight!.short_id, false), ]) .toMatchValues({ refreshStatus: { - [(dashboards['5'].tiles[0] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[0].insight!.short_id]: { refreshed: true, timer: expect.anything(), }, - [(dashboards['5'].tiles[1] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[1].insight!.short_id]: { refreshed: true, timer: expect.anything(), }, @@ -587,21 +578,18 @@ describe('dashboardLogic', () => { it('reloads selected items', async () => { await expectLogic(logic, () => { logic.actions.refreshAllDashboardItems({ - tiles: [dashboards['5'].tiles[0] as DashboardTile], + tiles: [dashboards['5'].tiles[0]], action: 'refresh_manual', }) }) .toFinishAllListeners() .toDispatchActions([ 'refreshAllDashboardItems', - logic.actionCreators.setRefreshStatuses( - [(dashboards['5'].tiles[0] as DashboardTile).insight!.short_id], - true - ), + logic.actionCreators.setRefreshStatuses([dashboards['5'].tiles[0].insight!.short_id], true), ]) .toMatchValues({ refreshStatus: { - [(dashboards['5'].tiles[0] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[0].insight!.short_id]: { loading: true, timer: expect.anything(), }, @@ -614,16 +602,12 @@ describe('dashboardLogic', () => { .toDispatchActionsInAnyOrder([ (a) => a.type === dashboardsModel.actionTypes.updateDashboardInsight && - a.payload.insight.short_id === - (dashboards['5'].tiles[0] as DashboardTile).insight!.short_id, - logic.actionCreators.setRefreshStatus( - (dashboards['5'].tiles[0] as DashboardTile).insight!.short_id, - false - ), + a.payload.insight.short_id === dashboards['5'].tiles[0].insight!.short_id, + logic.actionCreators.setRefreshStatus(dashboards['5'].tiles[0].insight!.short_id, false), ]) .toMatchValues({ refreshStatus: { - [(dashboards['5'].tiles[0] as DashboardTile).insight!.short_id]: { + [dashboards['5'].tiles[0].insight!.short_id]: { refreshed: true, timer: expect.anything(), }, @@ -861,4 +845,3 @@ describe('dashboardLogic', () => { ).toEqual([]) }) }) -/* eslint-enable @typescript-eslint/no-non-null-assertion */ diff --git a/frontend/src/scenes/dashboard/dashboardLogic.tsx b/frontend/src/scenes/dashboard/dashboardLogic.tsx index b5701be04d2d2..37e49db9c2bfa 100644 --- a/frontend/src/scenes/dashboard/dashboardLogic.tsx +++ b/frontend/src/scenes/dashboard/dashboardLogic.tsx @@ -233,7 +233,7 @@ export const dashboardLogic = kea([ filters: values.filters, }) } catch (e) { - lemonToast.error('Could not update dashboardFilters: ' + e) + lemonToast.error('Could not update dashboardFilters: ' + String(e)) return values.dashboard } }, @@ -267,7 +267,7 @@ export const dashboardLogic = kea([ tiles: values.tiles.filter((t) => t.id !== tile.id), } as DashboardType } catch (e) { - lemonToast.error('Could not remove tile from dashboard: ' + e) + lemonToast.error('Could not remove tile from dashboard: ' + String(e)) return values.dashboard } }, @@ -282,7 +282,7 @@ export const dashboardLogic = kea([ tiles: [newTile], } as Partial) } catch (e) { - lemonToast.error('Could not duplicate tile: ' + e) + lemonToast.error('Could not duplicate tile: ' + String(e)) return values.dashboard } }, @@ -466,7 +466,7 @@ export const dashboardLogic = kea([ tiles[tileIndex] = { ...tiles[tileIndex], insight: { - ...((tiles[tileIndex] as DashboardTile).insight as InsightModel), + ...(tiles[tileIndex].insight as InsightModel), name: item.name, last_modified_at: item.last_modified_at, }, @@ -963,7 +963,7 @@ export const dashboardLogic = kea([ ) actions.setRefreshStatus(insight.short_id) - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { type: 'insight_load', context: 'dashboard', primary_interaction_id: dashboardQueryId, @@ -1002,13 +1002,13 @@ export const dashboardLogic = kea([ insights_fetched: insights.length, insights_fetched_cached: 0, } - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { ...payload, is_primary_interaction: !initialLoad, }) if (initialLoad) { const { startTime, responseBytes } = values.dashboardLoadTimerData - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { ...payload, action: 'initial_load_full', time_to_see_data_ms: Math.floor(performance.now() - startTime), @@ -1019,9 +1019,13 @@ export const dashboardLogic = kea([ } }) - function loadNextPromise(): void { + async function loadNextPromise(): Promise { if (!cancelled && fetchItemFunctions.length > 0) { - fetchItemFunctions.shift()?.().then(loadNextPromise) + const nextPromise = fetchItemFunctions.shift() + if (nextPromise) { + await nextPromise() + await loadNextPromise() + } } } @@ -1068,7 +1072,7 @@ export const dashboardLogic = kea([ } }, loadDashboardItemsSuccess: function (...args) { - sharedListeners.reportLoadTiming(...args) + void sharedListeners.reportLoadTiming(...args) const dashboard = values.dashboard as DashboardType const { action, dashboardQueryId, startTime, responseBytes } = values.dashboardLoadTimerData @@ -1125,9 +1129,9 @@ export const dashboardLogic = kea([ is_primary_interaction: !initialLoad, } - captureTimeToSeeData(values.currentTeamId, payload) + void captureTimeToSeeData(values.currentTeamId, payload) if (initialLoad && allLoaded) { - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { ...payload, action: 'initial_load_full', is_primary_interaction: true, diff --git a/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.ts b/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.ts index 3f2e7f374d542..3d9ee6e1969c3 100644 --- a/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.ts +++ b/frontend/src/scenes/dashboard/dashboards/dashboardsLogic.ts @@ -49,7 +49,7 @@ export const dashboardsLogic = kea([ ], filters: [ - DEFAULT_FILTERS as DashboardsFilters, + DEFAULT_FILTERS, { setFilters: (state, { filters }) => objectClean({ diff --git a/frontend/src/scenes/data-management/actions/ActionsTable.tsx b/frontend/src/scenes/data-management/actions/ActionsTable.tsx index 55bb6d5bef1a5..6ef0ff70f5ffb 100644 --- a/frontend/src/scenes/data-management/actions/ActionsTable.tsx +++ b/frontend/src/scenes/data-management/actions/ActionsTable.tsx @@ -1,5 +1,4 @@ -import { LemonInput } from '@posthog/lemon-ui' -import { Radio } from 'antd' +import { LemonInput, LemonSegmentedButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { combineUrl } from 'kea-router' import api from 'lib/api' @@ -30,9 +29,9 @@ export function ActionsTable(): JSX.Element { const { actionsLoading } = useValues(actionsModel({ params: 'include_count=1' })) const { loadActions } = useActions(actionsModel) - const { filterByMe, searchTerm, actionsFiltered, shouldShowProductIntroduction, shouldShowEmptyState } = + const { filterType, searchTerm, actionsFiltered, shouldShowProductIntroduction, shouldShowEmptyState } = useValues(actionsLogic) - const { setFilterByMe, setSearchTerm } = useActions(actionsLogic) + const { setFilterType, setSearchTerm } = useActions(actionsLogic) const { hasAvailableFeature } = useValues(userLogic) const { updateHasSeenProductIntroFor } = useActions(userLogic) @@ -204,7 +203,7 @@ export function ActionsTable(): JSX.Element { - deleteWithUndo({ + void deleteWithUndo({ endpoint: api.actions.determineDeleteEndpoint(), object: action, callback: loadActions, @@ -239,7 +238,7 @@ export function ActionsTable(): JSX.Element { } /> )} - {(shouldShowEmptyState && filterByMe) || !shouldShowEmptyState ? ( + {(shouldShowEmptyState && filterType === 'me') || !shouldShowEmptyState ? (
- setFilterByMe(e.target.value)}> - All actions - My actions - +
) : null} - {(!shouldShowEmptyState || filterByMe) && ( + {(!shouldShowEmptyState || filterType === 'me') && ( <> ([ }), reducers({ filters: [ - cleanFilters({}) as Filters, + cleanFilters({}), { setFilters: (state, { filters }) => ({ ...state, diff --git a/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts b/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts index dc0e8c8319f7b..542ea4aade0be 100644 --- a/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts +++ b/frontend/src/scenes/data-management/properties/propertyDefinitionsTableLogic.ts @@ -194,7 +194,7 @@ export const propertyDefinitionsTableLogic = kea { const [type, index] = propertyType.split('::') actions.setFilters({ - type: type as string, + type: type, group_type_index: index ? +index : null, }) }, diff --git a/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx b/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx index 904c7d5d72a44..57598f3416c12 100644 --- a/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx +++ b/frontend/src/scenes/data-warehouse/external/DataWarehouseTables.tsx @@ -46,7 +46,7 @@ export function DataWarehouseTablesContainer(): JSX.Element { { - deleteWithUndo({ + void deleteWithUndo({ endpoint: `projects/${currentTeamId}/warehouse_tables`, object: { name: warehouseTable.name, id: warehouseTable.id }, callback: loadDataWarehouse, diff --git a/frontend/src/scenes/data-warehouse/new_table/dataWarehouseTableLogic.tsx b/frontend/src/scenes/data-warehouse/new_table/dataWarehouseTableLogic.tsx index 32b461d9889fb..9e60148514cf7 100644 --- a/frontend/src/scenes/data-warehouse/new_table/dataWarehouseTableLogic.tsx +++ b/frontend/src/scenes/data-warehouse/new_table/dataWarehouseTableLogic.tsx @@ -14,7 +14,8 @@ import { dataWarehouseSceneLogic } from '../external/dataWarehouseSceneLogic' import type { dataWarehouseTableLogicType } from './dataWarehouseTableLogicType' export interface TableLogicProps { - id: string | 'new' + /** A UUID or 'new'. */ + id: string } const NEW_WAREHOUSE_TABLE: DataWarehouseTable = { diff --git a/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx b/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx index 6db1f5a044b2a..e7d92fec2f9ea 100644 --- a/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx +++ b/frontend/src/scenes/data-warehouse/saved_queries/DataWarehouseSavedQueriesContainer.tsx @@ -69,7 +69,7 @@ export function DataWarehouseSavedQueriesContainer(): JSX.Element { { - deleteWithUndo({ + void deleteWithUndo({ endpoint: `projects/${currentTeamId}/warehouse_saved_queries`, object: { name: warehouseView.name, id: warehouseView.id }, callback: loadDataWarehouseSavedQueries, diff --git a/frontend/src/scenes/early-access-features/earlyAccessFeatureLogic.ts b/frontend/src/scenes/early-access-features/earlyAccessFeatureLogic.ts index 48260a4292114..88f12b1e3b27f 100644 --- a/frontend/src/scenes/early-access-features/earlyAccessFeatureLogic.ts +++ b/frontend/src/scenes/early-access-features/earlyAccessFeatureLogic.ts @@ -172,9 +172,9 @@ export const earlyAccessFeatureLogic = kea([ } }, })), - afterMount(async ({ props, actions }) => { + afterMount(({ props, actions }) => { if (props.id !== 'new') { - await actions.loadEarlyAccessFeature() + actions.loadEarlyAccessFeature() } }), ]) diff --git a/frontend/src/scenes/early-access-features/earlyAccessFeaturesLogic.ts b/frontend/src/scenes/early-access-features/earlyAccessFeaturesLogic.ts index 94680efa47c2b..f1c1c82479961 100644 --- a/frontend/src/scenes/early-access-features/earlyAccessFeaturesLogic.ts +++ b/frontend/src/scenes/early-access-features/earlyAccessFeaturesLogic.ts @@ -29,7 +29,7 @@ export const earlyAccessFeaturesLogic = kea([ ], ], }), - afterMount(async ({ actions }) => { - await actions.loadEarlyAccessFeatures() + afterMount(({ actions }) => { + actions.loadEarlyAccessFeatures() }), ]) diff --git a/frontend/src/scenes/experiments/Experiment.tsx b/frontend/src/scenes/experiments/Experiment.tsx index e2b7d9d4ea0fb..cce27d440bef3 100644 --- a/frontend/src/scenes/experiments/Experiment.tsx +++ b/frontend/src/scenes/experiments/Experiment.tsx @@ -253,7 +253,7 @@ export function Experiment(): JSX.Element { name={['parameters', 'feature_flag_variants', index]} >
([ })), forms(({ props }) => ({ secondaryMetricModal: { - defaults: defaultFormValuesGenerator(props.defaultAggregationType) as SecondaryMetricForm, + defaults: defaultFormValuesGenerator(props.defaultAggregationType), errors: () => ({}), submit: async () => { // We don't use the form submit anymore diff --git a/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx b/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx index ff9e6eed473fd..c27158a9983df 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagAutoRollout.tsx @@ -169,8 +169,8 @@ export function FeatureFlagAutoRollback({ readOnly }: FeatureFlagAutoRollbackPro
{sentryErrorCount ? ( - {humanFriendlyNumber(sentryErrorCount as number)} sentry - errors in the past 24 hours.{' '} + {humanFriendlyNumber(sentryErrorCount)} Sentry errors in the + past 24 hours.{' '} ) : ( @@ -205,7 +205,7 @@ export function FeatureFlagAutoRollback({ readOnly }: FeatureFlagAutoRollbackPro {humanFriendlyNumber( Math.round( - (sentryErrorCount as number) * + sentryErrorCount * (1 + (featureFlag.rollback_conditions[index] .threshold || 0) / diff --git a/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx b/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx index 7e53aff7f1e92..cf0bfb6666766 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx @@ -247,7 +247,7 @@ export function FeatureFlagReleaseConditions({ { - updateConditionSet(index, value as number) + updateConditionSet(index, value) }} value={group.rollout_percentage != null ? group.rollout_percentage : 100} min={0} diff --git a/frontend/src/scenes/feature-flags/FeatureFlags.tsx b/frontend/src/scenes/feature-flags/FeatureFlags.tsx index 630c4ab85d115..58d5b61452275 100644 --- a/frontend/src/scenes/feature-flags/FeatureFlags.tsx +++ b/frontend/src/scenes/feature-flags/FeatureFlags.tsx @@ -160,8 +160,8 @@ export function OverViewTab({ <> { - await copyToClipboard(featureFlag.key, 'feature flag key') + onClick={() => { + void copyToClipboard(featureFlag.key, 'feature flag key') }} fullWidth > @@ -212,7 +212,7 @@ export function OverViewTab({ { - deleteWithUndo({ + void deleteWithUndo({ endpoint: `projects/${currentTeamId}/feature_flags`, object: { name: featureFlag.key, id: featureFlag.id }, callback: loadFeatureFlags, diff --git a/frontend/src/scenes/feature-flags/featureFlagLogic.ts b/frontend/src/scenes/feature-flags/featureFlagLogic.ts index 3ee3e0ca046e6..6260616842a49 100644 --- a/frontend/src/scenes/feature-flags/featureFlagLogic.ts +++ b/frontend/src/scenes/feature-flags/featureFlagLogic.ts @@ -640,7 +640,7 @@ export const featureFlagLogic = kea([ actions.editFeatureFlag(false) }, deleteFeatureFlag: async ({ featureFlag }) => { - deleteWithUndo({ + await deleteWithUndo({ endpoint: `projects/${values.currentTeamId}/feature_flags`, object: { name: featureFlag.key, id: featureFlag.id }, callback: () => { @@ -781,7 +781,9 @@ export const featureFlagLogic = kea([ : 'copied' lemonToast.success(`Feature flag ${operation} successfully!`) } else { - lemonToast.error(`Error while saving feature flag: ${featureFlagCopy?.failed || featureFlagCopy}`) + lemonToast.error( + `Error while saving feature flag: ${JSON.stringify(featureFlagCopy?.failed) || featureFlagCopy}` + ) } actions.loadProjectsWithCurrentFlag() diff --git a/frontend/src/scenes/feedback/inAppFeedbackLogic.ts b/frontend/src/scenes/feedback/inAppFeedbackLogic.ts index 073cbc85fd536..646947084ab2b 100644 --- a/frontend/src/scenes/feedback/inAppFeedbackLogic.ts +++ b/frontend/src/scenes/feedback/inAppFeedbackLogic.ts @@ -62,7 +62,7 @@ export const inAppFeedbackLogic = kea([ }, ], dataTableQuery: [ - DEFAULT_DATATABLE_QUERY as DataTableNode, + DEFAULT_DATATABLE_QUERY, { setDataTableQuery: (_, { query }) => { if (query.kind === NodeKind.DataTableNode) { @@ -75,7 +75,7 @@ export const inAppFeedbackLogic = kea([ }, ], trendQuery: [ - DEFAULT_TREND_INSIGHT_VIZ_NODE as InsightVizNode, + DEFAULT_TREND_INSIGHT_VIZ_NODE, { setDataTableQuery: (_, { query }) => { if (query.kind === NodeKind.DataTableNode) { @@ -115,7 +115,7 @@ export const inAppFeedbackLogic = kea([ event: eventName, orderBy: ['-timestamp'], }) - return response.results as EventType[] + return response.results }, }, ], diff --git a/frontend/src/scenes/funnels/FunnelStepMore.tsx b/frontend/src/scenes/funnels/FunnelStepMore.tsx index faab54cf0ffac..8f62437b3a619 100644 --- a/frontend/src/scenes/funnels/FunnelStepMore.tsx +++ b/frontend/src/scenes/funnels/FunnelStepMore.tsx @@ -17,7 +17,7 @@ type FunnelStepMoreProps = { export function FunnelStepMore({ stepIndex }: FunnelStepMoreProps): JSX.Element | null { const { insightProps } = useValues(insightLogic) const { querySource } = useValues(funnelDataLogic(insightProps)) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const filterProps = cleanFilters(queryNodeToFilter(querySource!)) const aggregationGroupTypeIndex = querySource?.aggregation_group_type_index diff --git a/frontend/src/scenes/funnels/funnelDataLogic.ts b/frontend/src/scenes/funnels/funnelDataLogic.ts index 533cfc535c1ce..944ed7501434f 100644 --- a/frontend/src/scenes/funnels/funnelDataLogic.ts +++ b/frontend/src/scenes/funnels/funnelDataLogic.ts @@ -14,7 +14,6 @@ import { FunnelConversionWindow, FunnelConversionWindowTimeUnit, FunnelResultType, - FunnelStep, FunnelStepReference, FunnelStepWithConversionMetrics, FunnelStepWithNestedBreakdown, @@ -165,7 +164,7 @@ export const funnelDataLogic = kea([ : breakdown?.breakdown ?? undefined return aggregateBreakdownResult(results, breakdownProperty).sort((a, b) => a.order - b.order) } - return (results as FunnelStep[]).sort((a, b) => a.order - b.order) + return results.sort((a, b) => a.order - b.order) } else { return [] } diff --git a/frontend/src/scenes/funnels/funnelUtils.ts b/frontend/src/scenes/funnels/funnelUtils.ts index 5c3726384b410..b19dcd865fed2 100644 --- a/frontend/src/scenes/funnels/funnelUtils.ts +++ b/frontend/src/scenes/funnels/funnelUtils.ts @@ -601,7 +601,7 @@ export const parseDisplayNameForCorrelation = ( first_value = autoCaptureEventToDescription({ ...record.event, event: '$autocapture', - }) as string + }) return { first_value, second_value } } else { // FunnelCorrelationResultsType.EventWithProperties diff --git a/frontend/src/scenes/ingestion/CardContainer.tsx b/frontend/src/scenes/ingestion/CardContainer.tsx deleted file mode 100644 index 14feae01c4bf4..0000000000000 --- a/frontend/src/scenes/ingestion/CardContainer.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import './panels/Panels.scss' - -import { IngestionState } from 'scenes/ingestion/ingestionLogic' - -import { PanelFooter } from './panels/PanelComponents' - -export function CardContainer({ - children, - nextProps, - onContinue, - finalStep = false, - showInviteTeamMembers = true, -}: { - children: React.ReactNode - nextProps?: Partial - onContinue?: () => void - finalStep?: boolean - showInviteTeamMembers?: boolean -}): JSX.Element { - return ( - // We want a forced width for this view only - // eslint-disable-next-line react/forbid-dom-props -
- {children} -
- {nextProps && ( - - )} -
-
- ) -} diff --git a/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx b/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx deleted file mode 100644 index 439390bd83164..0000000000000 --- a/frontend/src/scenes/ingestion/IngestionInviteMembersButton.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { LemonButton } from '@posthog/lemon-ui' -import { useActions } from 'kea' -import { IconArrowRight } from 'lib/lemon-ui/icons' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' - -export function IngestionInviteMembersButton(): JSX.Element { - const { showInviteModal } = useActions(inviteLogic) - const { reportInviteMembersButtonClicked } = useActions(eventUsageLogic) - - return ( - } - className="mt-6" - onClick={() => { - showInviteModal() - reportInviteMembersButtonClicked() - }} - > - Invite a team member to help with this step - - ) -} diff --git a/frontend/src/scenes/ingestion/IngestionWizard.scss b/frontend/src/scenes/ingestion/IngestionWizard.scss deleted file mode 100644 index e1d2b422bdf7c..0000000000000 --- a/frontend/src/scenes/ingestion/IngestionWizard.scss +++ /dev/null @@ -1,77 +0,0 @@ -.IngestionContainer { - display: flex; - height: 100%; - align-items: center; - justify-content: center; - padding: 2rem; - flex-direction: column; - width: 100%; -} - -.IngestionContent { - .BridgePage__content { - max-width: 700px; - } -} - -.IngestionTopbar { - border-bottom: 1px solid var(--border); - padding: 0.25rem 1rem; - display: flex; - justify-content: space-between; - position: sticky; - top: 0; - background-color: white; - width: 100%; - z-index: 10; - - .help-button { - margin-right: 1rem; - } -} - -.platform-item { - margin-right: 10px; - padding: 10px; - padding-left: 20px; - padding-right: 20px; - border: 1px solid gray; - border-radius: 2px; - cursor: pointer; -} - -.platform-item:hover { - background-color: gainsboro; -} - -.selectable-item:hover { - background-color: gainsboro; - cursor: pointer; -} - -.IngestionSidebar__bottom { - margin-top: auto; - - .popover { - padding-left: 0.5rem; - padding-right: 0.5rem; - } -} - -.IngestionSidebar__help { - display: flex; - flex-direction: column; - font-weight: 500; - color: var(--primary); - margin-top: 1rem; -} - -.IngestionSidebar__steps { - color: var(--muted-alt); - font-size: 14px; - - .LemonButton { - font-weight: 600; - margin-bottom: 0.5rem; - } -} diff --git a/frontend/src/scenes/ingestion/IngestionWizard.tsx b/frontend/src/scenes/ingestion/IngestionWizard.tsx deleted file mode 100644 index bc874c0c134c9..0000000000000 --- a/frontend/src/scenes/ingestion/IngestionWizard.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import './IngestionWizard.scss' - -import { useActions, useValues } from 'kea' -import { router } from 'kea-router' -import { BridgePage } from 'lib/components/BridgePage/BridgePage' -import { HelpButton } from 'lib/components/HelpButton/HelpButton' -import { FEATURE_FLAGS } from 'lib/constants' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { useEffect } from 'react' -import { INGESTION_VIEWS, ingestionLogic } from 'scenes/ingestion/ingestionLogic' -import { FrameworkPanel } from 'scenes/ingestion/panels/FrameworkPanel' -import { InstructionsPanel } from 'scenes/ingestion/panels/InstructionsPanel' -import { PlatformPanel } from 'scenes/ingestion/panels/PlatformPanel' -import { SuperpowersPanel } from 'scenes/ingestion/panels/SuperpowersPanel' -import { VerificationPanel } from 'scenes/ingestion/panels/VerificationPanel' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' -import { InviteModal } from 'scenes/settings/organization/InviteModal' -import { urls } from 'scenes/urls' - -import { SitePopover } from '~/layout/navigation/TopBar/SitePopover' -import { Logo } from '~/toolbar/assets/Logo' - -import { BillingPanel } from './panels/BillingPanel' -import { GeneratingDemoDataPanel } from './panels/GeneratingDemoDataPanel' -import { InviteTeamPanel } from './panels/InviteTeamPanel' -import { NoDemoIngestionPanel } from './panels/NoDemoIngestionPanel' -import { PanelHeader } from './panels/PanelComponents' -import { TeamInvitedPanel } from './panels/TeamInvitedPanel' -import { ThirdPartyPanel } from './panels/ThirdPartyPanel' -import { Sidebar } from './Sidebar' - -export function IngestionWizard(): JSX.Element { - const { currentView, platform } = useValues(ingestionLogic) - const { reportIngestionLandingSeen } = useActions(eventUsageLogic) - const { featureFlags } = useValues(featureFlagLogic) - - useEffect(() => { - if (!platform) { - reportIngestionLandingSeen() - } - }, [platform]) - - if (featureFlags[FEATURE_FLAGS.PRODUCT_SPECIFIC_ONBOARDING] === 'test') { - router.actions.replace(urls.products()) - } - - return ( - - {currentView === INGESTION_VIEWS.BILLING && } - {currentView === INGESTION_VIEWS.SUPERPOWERS && } - {currentView === INGESTION_VIEWS.INVITE_TEAM && } - {currentView === INGESTION_VIEWS.TEAM_INVITED && } - {currentView === INGESTION_VIEWS.CHOOSE_PLATFORM && } - {currentView === INGESTION_VIEWS.CHOOSE_FRAMEWORK && } - {currentView === INGESTION_VIEWS.WEB_INSTRUCTIONS && } - {currentView === INGESTION_VIEWS.VERIFICATION && } - {currentView === INGESTION_VIEWS.GENERATING_DEMO_DATA && } - {currentView === INGESTION_VIEWS.CHOOSE_THIRD_PARTY && } - {currentView === INGESTION_VIEWS.NO_DEMO_INGESTION && } - - ) -} - -function IngestionContainer({ children }: { children: React.ReactNode }): JSX.Element { - const { isInviteModalShown } = useValues(inviteLogic) - const { hideInviteModal } = useActions(inviteLogic) - const { isSmallScreen } = useValues(ingestionLogic) - - return ( -
-
- -
- - -
-
-
- {!isSmallScreen && } - {/*
} - className="IngestionContent h-full" - fullScreen={false} - > - {children} - -
- -
- ) -} diff --git a/frontend/src/scenes/ingestion/Sidebar.tsx b/frontend/src/scenes/ingestion/Sidebar.tsx deleted file mode 100644 index 6430aeb5f27bd..0000000000000 --- a/frontend/src/scenes/ingestion/Sidebar.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import './IngestionWizard.scss' - -import { Link } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { IconArticle, IconQuestionAnswer } from 'lib/lemon-ui/icons' -import { LemonButton, LemonButtonWithDropdown } from 'lib/lemon-ui/LemonButton' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { Lettermark } from 'lib/lemon-ui/Lettermark' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { organizationLogic } from 'scenes/organizationLogic' - -import { ProjectSwitcherOverlay } from '~/layout/navigation/ProjectSwitcher' -import { HelpType } from '~/types' - -import { ingestionLogic } from './ingestionLogic' - -const HELP_UTM_TAGS = '?utm_medium=in-product-onboarding&utm_campaign=help-button-sidebar' - -export function Sidebar(): JSX.Element { - const { currentStep, sidebarSteps, isProjectSwitcherShown } = useValues(ingestionLogic) - const { sidebarStepClick, toggleProjectSwitcher, hideProjectSwitcher } = useActions(ingestionLogic) - const { reportIngestionHelpClicked, reportIngestionSidebarButtonClicked } = useActions(eventUsageLogic) - const { currentOrganization } = useValues(organizationLogic) - - const currentIndex = sidebarSteps.findIndex((x) => x === currentStep) - - return ( -
-
-
- {sidebarSteps.map((step: string, index: number) => ( - currentIndex} - onClick={() => { - sidebarStepClick(step) - reportIngestionSidebarButtonClicked(step) - }} - > - {step} - - ))} -
-
- {currentOrganization?.teams && currentOrganization.teams.length > 1 && ( - <> - } - onClick={() => toggleProjectSwitcher()} - dropdown={{ - visible: isProjectSwitcherShown, - onClickOutside: hideProjectSwitcher, - overlay: , - actionable: true, - placement: 'top-end', - }} - type="secondary" - fullWidth - > - Switch project - - - - )} -
- - } - fullWidth - onClick={() => { - reportIngestionHelpClicked(HelpType.Slack) - }} - > - Get support on Slack - - - - } - fullWidth - onClick={() => { - reportIngestionHelpClicked(HelpType.Docs) - }} - > - Read our documentation - - -
-
-
-
- ) -} diff --git a/frontend/src/scenes/ingestion/constants.tsx b/frontend/src/scenes/ingestion/constants.tsx deleted file mode 100644 index da51d53e7ab66..0000000000000 --- a/frontend/src/scenes/ingestion/constants.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { PlatformType } from 'scenes/ingestion/types' - -import { RSS, Segment } from './panels/ThirdPartyIcons' - -export const TECHNICAL = 'TECHNICAL' -export const PLATFORM_TYPE = 'PLATFORM_TYPE' -export const FRAMEWORK = 'FRAMEWORK' -export const INSTRUCTIONS = 'INSTRUCTIONS' -export const VERIFICATION = 'VERIFICATION' - -export const WEB = 'web' -export const MOBILE = 'mobile' -export const BACKEND = 'backend' -export const THIRD_PARTY = 'third-party' -export const platforms: PlatformType[] = [WEB, MOBILE, BACKEND] - -export const NODEJS = 'NODEJS' -export const GO = 'GO' -export const RUBY = 'RUBY' -export const PYTHON = 'PYTHON' -export const PHP = 'PHP' -export const ELIXIR = 'ELIXIR' -export const API = 'API' - -export const ANDROID = 'ANDROID' -export const IOS = 'IOS' -export const REACT_NATIVE = 'REACT_NATIVE' -export const FLUTTER = 'FLUTTER' - -export const httpFrameworks = { - [API]: 'HTTP API', -} -export const webFrameworks = { - [NODEJS]: 'Node.js', - [GO]: 'Go', - [RUBY]: 'Ruby', - [PYTHON]: 'Python', - [PHP]: 'PHP', - [ELIXIR]: 'Elixir', -} - -export const mobileFrameworks = { - [ANDROID]: 'Android', - [IOS]: 'iOS', - [REACT_NATIVE]: 'React Native', - [FLUTTER]: 'Flutter', -} - -export const allFrameworks = { - ...webFrameworks, - ...mobileFrameworks, - ...httpFrameworks, -} -export interface ThirdPartySource { - name: string - icon: JSX.Element - docsLink: string - aboutLink: string - labels?: string[] - description?: string -} - -export const thirdPartySources: ThirdPartySource[] = [ - { - name: 'Segment', - icon: , - docsLink: 'https://posthog.com/docs/integrate/third-party/segment', - aboutLink: 'https://segment.com', - }, - { - name: 'Rudderstack', - icon: ( - - ), - docsLink: 'https://posthog.com/docs/integrate/third-party/rudderstack', - aboutLink: 'https://rudderstack.com', - }, - { - name: 'RSS items', - description: 'Send events from releases, blog posts, status pages, or any other RSS feed into PostHog', - icon: , - docsLink: 'https://posthog.com/tutorials/rss-item-capture', - aboutLink: 'https://en.wikipedia.org/wiki/RSS', - }, -] diff --git a/frontend/src/scenes/ingestion/frameworks/APIInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/APIInstructions.tsx deleted file mode 100644 index 1d5b581634bd1..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/APIInstructions.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' - -function APISnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - const url = window.location.origin - - return ( - - {'POST ' + - url + - '/capture/\nContent-Type: application/json\n\n{\n\t"api_key": "' + - currentTeam?.api_token + - '",\n\t"event": "[event name]",\n\t"properties": {\n\t\t"distinct_id": "[your users\' distinct id]",\n\t\t"key1": "value1",\n\t\t"key2": "value2"\n\t},\n\t"timestamp": "[optional timestamp in ISO 8601 format]"\n}'} - - ) -} - -export function APIInstructions(): JSX.Element { - return ( - <> -

Usage

- - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/AndroidInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/AndroidInstructions.tsx deleted file mode 100644 index 90fcf1ff6cd20..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/AndroidInstructions.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' - -function AndroidInstallSnippet(): JSX.Element { - return ( - - {`dependencies { - implementation 'com.posthog.android:posthog:1.+' -}`} - - ) -} - -function AndroidSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`public class SampleApp extends Application { - private static final String POSTHOG_API_KEY = "${currentTeam?.api_token}"; - private static final String POSTHOG_HOST = "${window.location.origin}"; - - @Override - public void onCreate() { - // Create a PostHog client with the given context, API key and host - PostHog posthog = new PostHog.Builder(this, POSTHOG_API_KEY, POSTHOG_HOST) - .captureApplicationLifecycleEvents() // Record certain application events automatically! - .recordScreenViews() // Record screen views automatically! - .build(); - - // Set the initialized instance as a globally accessible instance - PostHog.setSingletonInstance(posthog); - - // Now any time you call PostHog.with, the custom instance will be returned - PostHog posthog = PostHog.with(this); - }`} - - ) -} - -function AndroidCaptureSnippet(): JSX.Element { - return PostHog.with(this).capture("test-event"); -} - -export function AndroidInstructions(): JSX.Element { - return ( - <> -

Install

- -

Configure

- -

Send an Event

- - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/ElixirInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/ElixirInstructions.tsx deleted file mode 100644 index 982b2d3fe3859..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/ElixirInstructions.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' - -function ElixirInstallSnippet(): JSX.Element { - return ( - - {'def deps do\n [\n {:posthog, "~> 0.1"}\n ]\nend'} - - ) -} - -function ElixirSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - const url = window.location.origin - - return ( - - {'config :posthog,\n api_url: "' + url + '",\n api_key: "' + currentTeam?.api_token + '"'} - - ) -} - -export function ElixirInstructions(): JSX.Element { - return ( - <> -

Install

- -

Configure

- - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/FlutterInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/FlutterInstructions.tsx deleted file mode 100644 index 1164221fc1aaf..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/FlutterInstructions.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' - -function FlutterInstallSnippet(): JSX.Element { - return {'posthog_flutter: # insert version number'} -} - -function FlutterCaptureSnippet(): JSX.Element { - return ( - - { - "import 'package:posthog_flutter/posthog_flutter.dart';\n\nPosthog().screen(\n\tscreenName: 'Example Screen',\n);" - } - - ) -} - -function FlutterAndroidSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - const url = window.location.origin - - return ( - - {'\n\t\n\t\t[...]\n\t\n\t\n\t\n\t\n\t\n'} - - ) -} - -function FlutterIOSSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - const url = window.location.origin - - return ( - - {'\n\t[...]\n\tcom.posthog.posthog.API_KEY\n\t' + - currentTeam?.api_token + - '\n\tcom.posthog.posthog.POSTHOG_HOST\n\t' + - url + - '\n\tcom.posthog.posthog.TRACK_APPLICATION_LIFECYCLE_EVENTS\n\t\n\t[...]\n'} - - ) -} - -export function FlutterInstructions(): JSX.Element { - return ( - <> -

Install

- -

Android Setup

-

{'Add these values in AndroidManifest.xml'}

- -

iOS Setup

-

{'Add these values in Info.plist'}

- -

Send an Event

- - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/GoInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/GoInstructions.tsx deleted file mode 100644 index 591ac761d0a10..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/GoInstructions.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' - -function GoInstallSnippet(): JSX.Element { - return {'go get "github.com/posthog/posthog-go"'} -} - -function GoSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`package main -import ( - "github.com/posthog/posthog-go" -) -func main() { - client, _ := posthog.NewWithConfig("${currentTeam?.api_token}", posthog.Config{Endpoint: "${window.location.origin}"}) - defer client.Close() -}`} - - ) -} - -function GoCaptureSnippet(): JSX.Element { - return ( - - {'client.Enqueue(posthog.Capture{\n DistinctId: "test-user",\n Event: "test-snippet",\n})'} - - ) -} - -export function GoInstructions(): JSX.Element { - return ( - <> -

Install

- -

Configure

- -

Send an Event

- - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/NodeInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/NodeInstructions.tsx deleted file mode 100644 index 3e86335aae077..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/NodeInstructions.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' - -function NodeInstallSnippet(): JSX.Element { - return ( - - {`npm install posthog-node -# OR -yarn add posthog-node -# OR -pnpm add posthog-node`} - - ) -} - -function NodeSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`import { PostHog } from 'posthog-node' - -const client = new PostHog( - '${currentTeam?.api_token}', - { host: '${window.location.origin}' } -)`} - - ) -} - -function NodeCaptureSnippet(): JSX.Element { - return ( - - {`client.capture({ - distinctId: 'test-id', - event: 'test-event' -}) - -// Send queued events immediately. Use for example in a serverless environment -// where the program may terminate before everything is sent -client.flush()`} - - ) -} - -export function NodeInstructions(): JSX.Element { - return ( - <> -

Install

- -

Configure

- -

Send an Event

- - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/PHPInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/PHPInstructions.tsx deleted file mode 100644 index 3c344eb83b61a..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/PHPInstructions.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' - -function PHPConfigSnippet(): JSX.Element { - return ( - - {`{ - "require": { - "posthog/posthog-php": "1.0.*" - } -}`} - - ) -} - -function PHPInstallSnippet(): JSX.Element { - return {'php composer.phar install'} -} - -function PHPSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`PostHog::init('${currentTeam?.api_token}', - array('host' => '${window.location.origin}') -);`} - - ) -} - -function PHPCaptureSnippet(): JSX.Element { - return ( - - {"PostHog::capture(array(\n 'distinctId' => 'test-user',\n 'event' => 'test-event'\n));"} - - ) -} - -export function PHPInstructions(): JSX.Element { - return ( - <> -

Dependency Setup

- -

Install

- -

Configure

- -

Send an Event

- - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/PythonInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/PythonInstructions.tsx deleted file mode 100644 index a494dadfcdc22..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/PythonInstructions.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' - -function PythonInstallSnippet(): JSX.Element { - return {'pip install posthog'} -} - -function PythonSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`from posthog import Posthog - -posthog = Posthog(project_api_key='${currentTeam?.api_token}', host='${window.location.origin}') - - `} - - ) -} - -function PythonCaptureSnippet(): JSX.Element { - return {"posthog.capture('test-id', 'test-event')"} -} - -export function PythonInstructions(): JSX.Element { - return ( - <> -

Install

- -

Configure

- -

Send an Event

- - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/ReactNativeInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/ReactNativeInstructions.tsx deleted file mode 100644 index 677e381d2bfc7..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/ReactNativeInstructions.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Link } from '@posthog/lemon-ui' -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' - -export function RNInstructions(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - const url = window.location.origin - - return ( - <> -

Install

- - {`# Expo apps -expo install posthog-react-native expo-file-system expo-application expo-device expo-localization - -# Standard React Native apps -yarn add posthog-react-native @react-native-async-storage/async-storage react-native-device-info -# or -npm i -s posthog-react-native @react-native-async-storage/async-storage react-native-device-info - -# for iOS -cd ios -pod install`} - -

Configure

-

- PostHog is most easily used via the PostHogProvider component but if you need to - instantiate it directly,{' '} - - check out the docs - {' '} - which explain how to do this correctly. -

- - {`// App.(js|ts) -import { PostHogProvider } from 'posthog-react-native' -... - -export function MyApp() { - return ( - - - - ) -}`} - -

Send an Event

- {`// With hooks -import { usePostHog } from 'posthog-react-native' - -const MyComponent = () => { - const posthog = usePostHog() - - useEffect(() => { - posthog.capture("MyComponent loaded", { foo: "bar" }) - }, []) -} - `} - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/RubyInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/RubyInstructions.tsx deleted file mode 100644 index e57b82cfff05b..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/RubyInstructions.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' - -function RubyInstallSnippet(): JSX.Element { - return {'gem "posthog-ruby"'} -} - -function RubySetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`posthog = PostHog::Client.new({ - api_key: "${currentTeam?.api_token}", - host: "${window.location.origin}", - on_error: Proc.new { |status, msg| print msg } -})`} - - ) -} - -function RubyCaptureSnippet(): JSX.Element { - return ( - - {"posthog.capture({\n distinct_id: 'test-id',\n event: 'test-event'})"} - - ) -} - -export function RubyInstructions(): JSX.Element { - return ( - <> -

Install

- -

Configure

- -

Send an Event

- - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/WebInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/WebInstructions.tsx deleted file mode 100644 index 673af39613843..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/WebInstructions.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { JSSnippet } from 'lib/components/JSSnippet' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { Link } from 'lib/lemon-ui/Link' -import { teamLogic } from 'scenes/teamLogic' - -function JSInstallSnippet(): JSX.Element { - return ( - - {['npm install posthog-js', '# OR', 'yarn add posthog-js', '# OR', 'pnpm add posthog-js'].join('\n')} - - ) -} - -function JSSetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {[ - "import posthog from 'posthog-js'", - '', - `posthog.init('${currentTeam?.api_token}', { api_host: '${window.location.origin}' })`, - ].join('\n')} - - ) -} - -function JSEventSnippet(): JSX.Element { - return ( - {`posthog.capture('my event', { property: 'value' })`} - ) -} - -export function WebInstructions(): JSX.Element { - return ( - <> -

Connect your web app or product

-
-

Option 1. Code snippet

-
- Recommended -
-
-

- Just add this snippet to your website and we'll automatically capture page views, sessions and all - relevant interactions within your website.{' '} - - Learn more - - . -

-

Install the snippet

-

- Insert this snippet in your website within the <head> tag. -

-

Send events

-

Visit your site and click around to generate some initial events.

- -
-

Option 2. Javascript Library

-
-

- Use this option if you want more granular control of how PostHog runs in your website and the events you - capture. Recommended for teams with more stable products and more defined analytics requirements.{' '} - - Learn more - - . -

-

Install the package

- -

- Configure & initialize (see more{' '} - - configuration options - - ) -

- -

Send your first event

- - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/iOSInstructions.tsx b/frontend/src/scenes/ingestion/frameworks/iOSInstructions.tsx deleted file mode 100644 index 8577a68858527..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/iOSInstructions.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useValues } from 'kea' -import { CodeSnippet, Language } from 'lib/components/CodeSnippet' -import { teamLogic } from 'scenes/teamLogic' - -function IOSInstallSnippet(): JSX.Element { - return ( - - {'pod "PostHog", "~> 1.0" # Cocoapods \n# OR \ngithub "posthog/posthog-ios" # Carthage'} - - ) -} - -function IOS_OBJ_C_SetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`#import \n#import \n\nPHGPostHogConfiguration *configuration = [PHGPostHogConfiguration configurationWithApiKey:@"${currentTeam?.api_token}" host:@"${window.location.origin}"];\n\nconfiguration.captureApplicationLifecycleEvents = YES; // Record certain application events automatically!\nconfiguration.recordScreenViews = YES; // Record screen views automatically!\n\n[PHGPostHog setupWithConfiguration:configuration];`} - - ) -} - -function IOS_SWIFT_SetupSnippet(): JSX.Element { - const { currentTeam } = useValues(teamLogic) - - return ( - - {`import PostHog\n\nlet configuration = PHGPostHogConfiguration(apiKey: "${currentTeam?.api_token}", host: "${window.location.origin}")\n\nconfiguration.captureApplicationLifecycleEvents = true; // Record certain application events automatically!\nconfiguration.recordScreenViews = true; // Record screen views automatically!\n\nPHGPostHog.setup(with: configuration)\nlet posthog = PHGPostHog.shared()`} - - ) -} - -function IOS_OBJ_C_CaptureSnippet(): JSX.Element { - return ( - - {'[[PHGPostHog sharedPostHog] capture:@"Test Event"];'} - - ) -} - -function IOS_SWIFT_CaptureSnippet(): JSX.Element { - return {'posthog.capture("Test Event")'} -} - -export function IOSInstructions(): JSX.Element { - return ( - <> -

Install

- -

Configure Swift

- -

Or configure Objective-C

- -

Send an event with swift

- -

Send an event with Objective-C

- - - ) -} diff --git a/frontend/src/scenes/ingestion/frameworks/index.tsx b/frontend/src/scenes/ingestion/frameworks/index.tsx deleted file mode 100644 index 4d1572820313f..0000000000000 --- a/frontend/src/scenes/ingestion/frameworks/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -export * from './AndroidInstructions' -export * from './APIInstructions' -export * from './ElixirInstructions' -export * from './FlutterInstructions' -export * from './GoInstructions' -export * from './iOSInstructions' -export * from './NodeInstructions' -export * from './PHPInstructions' -export * from './PythonInstructions' -export * from './ReactNativeInstructions' -export * from './RubyInstructions' diff --git a/frontend/src/scenes/ingestion/ingestionLogic.ts b/frontend/src/scenes/ingestion/ingestionLogic.ts deleted file mode 100644 index ef2047565c4c3..0000000000000 --- a/frontend/src/scenes/ingestion/ingestionLogic.ts +++ /dev/null @@ -1,719 +0,0 @@ -import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' -import { loaders } from 'kea-loaders' -import { actionToUrl, combineUrl, router, urlToAction } from 'kea-router' -import { subscriptions } from 'kea-subscriptions' -import { windowValues } from 'kea-window-values' -import api from 'lib/api' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { getBreakpoint } from 'lib/utils/responsiveUtils' -import { Framework, PlatformType } from 'scenes/ingestion/types' -import { preflightLogic } from 'scenes/PreflightCheck/preflightLogic' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' -import { teamLogic } from 'scenes/teamLogic' -import { urls } from 'scenes/urls' - -import { TeamType } from '~/types' - -import { API, BACKEND, MOBILE, THIRD_PARTY, ThirdPartySource, thirdPartySources, WEB } from './constants' -import type { ingestionLogicType } from './ingestionLogicType' - -export enum INGESTION_STEPS { - START = 'Get started', - PLATFORM = 'Select your platform', - CONNECT_PRODUCT = 'Connect your product', - VERIFY = 'Listen for events', - SUPERPOWERS = 'Enable superpowers', - BILLING = 'Add payment method', - DONE = 'Done!', -} - -export enum INGESTION_STEPS_WITHOUT_BILLING { - START = 'Get started', - PLATFORM = 'Select your platform', - CONNECT_PRODUCT = 'Connect your product', - VERIFY = 'Listen for events', - SUPERPOWERS = 'Enable superpowers', - DONE = 'Done!', -} - -export enum INGESTION_VIEWS { - BILLING = 'billing', - SUPERPOWERS = 'superpowers', - INVITE_TEAM = 'invite-team', - TEAM_INVITED = 'post-invite-team', - CHOOSE_PLATFORM = 'choose-platform', - VERIFICATION = 'verification', - WEB_INSTRUCTIONS = 'web-instructions', - CHOOSE_FRAMEWORK = 'choose-framework', - GENERATING_DEMO_DATA = 'generating-demo-data', - CHOOSE_THIRD_PARTY = 'choose-third-party', - NO_DEMO_INGESTION = 'no-demo-ingestion', -} - -export const INGESTION_VIEW_TO_STEP = { - [INGESTION_VIEWS.BILLING]: INGESTION_STEPS.BILLING, - [INGESTION_VIEWS.SUPERPOWERS]: INGESTION_STEPS.SUPERPOWERS, - [INGESTION_VIEWS.INVITE_TEAM]: INGESTION_STEPS.START, - [INGESTION_VIEWS.TEAM_INVITED]: INGESTION_STEPS.START, - [INGESTION_VIEWS.NO_DEMO_INGESTION]: INGESTION_STEPS.START, - [INGESTION_VIEWS.CHOOSE_PLATFORM]: INGESTION_STEPS.PLATFORM, - [INGESTION_VIEWS.VERIFICATION]: INGESTION_STEPS.VERIFY, - [INGESTION_VIEWS.WEB_INSTRUCTIONS]: INGESTION_STEPS.CONNECT_PRODUCT, - [INGESTION_VIEWS.CHOOSE_FRAMEWORK]: INGESTION_STEPS.CONNECT_PRODUCT, - [INGESTION_VIEWS.GENERATING_DEMO_DATA]: INGESTION_STEPS.CONNECT_PRODUCT, - [INGESTION_VIEWS.CHOOSE_THIRD_PARTY]: INGESTION_STEPS.CONNECT_PRODUCT, -} - -export type IngestionState = { - platform: PlatformType - framework: Framework - readyToVerify: boolean - showSuperpowers: boolean - showBilling: boolean - hasInvitedMembers: boolean | null - isTechnicalUser: boolean | null - isDemoProject: boolean | null - generatingDemoData: boolean | null -} - -const viewToState = (view: string, props: IngestionState): IngestionState => { - switch (view) { - case INGESTION_VIEWS.INVITE_TEAM: - return { - isTechnicalUser: null, - hasInvitedMembers: null, - platform: null, - framework: null, - readyToVerify: false, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - case INGESTION_VIEWS.TEAM_INVITED: - return { - isTechnicalUser: false, - hasInvitedMembers: true, - platform: null, - framework: null, - readyToVerify: false, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - case INGESTION_VIEWS.BILLING: - return { - isTechnicalUser: null, - hasInvitedMembers: null, - platform: props.platform, - framework: props.framework, - readyToVerify: false, - showSuperpowers: false, - showBilling: true, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - case INGESTION_VIEWS.VERIFICATION: - return { - isTechnicalUser: true, - hasInvitedMembers: null, - platform: props.platform, - framework: props.framework, - readyToVerify: true, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - case INGESTION_VIEWS.SUPERPOWERS: - return { - isTechnicalUser: null, - hasInvitedMembers: null, - platform: props.platform, - framework: props.framework, - readyToVerify: false, - showSuperpowers: true, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - case INGESTION_VIEWS.CHOOSE_PLATFORM: - return { - isTechnicalUser: true, - hasInvitedMembers: null, - platform: null, - framework: null, - readyToVerify: false, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - - case INGESTION_VIEWS.CHOOSE_FRAMEWORK: - return { - isTechnicalUser: true, - hasInvitedMembers: null, - platform: props.platform, - framework: null, - readyToVerify: false, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } - } - return { - isTechnicalUser: null, - hasInvitedMembers: null, - platform: null, - framework: null, - readyToVerify: false, - showSuperpowers: false, - showBilling: false, - isDemoProject: props.isDemoProject, - generatingDemoData: false, - } -} - -export const ingestionLogic = kea([ - path(['scenes', 'ingestion', 'ingestionLogic']), - connect({ - values: [ - featureFlagLogic, - ['featureFlags'], - teamLogic, - ['currentTeam'], - preflightLogic, - ['preflight'], - inviteLogic, - ['isInviteModalShown'], - ], - actions: [ - teamLogic, - ['updateCurrentTeamSuccess', 'createTeamSuccess'], - inviteLogic, - ['inviteTeamMembersSuccess'], - ], - }), - actions({ - setState: ({ - isTechnicalUser, - hasInvitedMembers, - platform, - framework, - readyToVerify, - showSuperpowers, - showBilling, - isDemoProject, - generatingDemoData, - }: IngestionState) => ({ - isTechnicalUser, - hasInvitedMembers, - platform, - framework, - readyToVerify, - showSuperpowers, - showBilling, - isDemoProject, - generatingDemoData, - }), - setInstructionsModal: (isOpen: boolean) => ({ isOpen }), - setThirdPartySource: (sourceIndex: number) => ({ sourceIndex }), - completeOnboarding: true, - setCurrentStep: (currentStep: string) => ({ currentStep }), - sidebarStepClick: (step: string) => ({ step }), - next: (props: Partial) => props, - onBack: true, - goToView: (view: INGESTION_VIEWS) => ({ view }), - setSidebarSteps: (steps: string[]) => ({ steps }), - setPollTimeout: (pollTimeout: number) => ({ pollTimeout }), - toggleProjectSwitcher: true, - hideProjectSwitcher: true, - }), - windowValues({ - isSmallScreen: (window: Window) => window.innerWidth < getBreakpoint('md'), - }), - reducers({ - isTechnicalUser: [ - null as null | boolean, - { - setState: (_, { isTechnicalUser }) => isTechnicalUser, - }, - ], - hasInvitedMembers: [ - null as null | boolean, - { - setState: (_, { hasInvitedMembers }) => hasInvitedMembers, - }, - ], - platform: [ - null as null | PlatformType, - { - setState: (_, { platform }) => platform, - }, - ], - framework: [ - null as null | Framework, - { - setState: (_, { framework }) => (framework ? (framework.toUpperCase() as Framework) : null), - }, - ], - readyToVerify: [ - false, - { - setState: (_, { readyToVerify }) => readyToVerify, - }, - ], - showSuperpowers: [ - false, - { - setState: (_, { showSuperpowers }) => showSuperpowers, - }, - ], - showBilling: [ - false, - { - setState: (_, { showBilling }) => showBilling, - }, - ], - instructionsModalOpen: [ - false as boolean, - { - setInstructionsModal: (_, { isOpen }) => isOpen, - }, - ], - thirdPartyIntegrationSource: [ - null as ThirdPartySource | null, - { - setThirdPartySource: (_, { sourceIndex }) => thirdPartySources[sourceIndex], - }, - ], - sidebarSteps: [ - Object.values(INGESTION_STEPS_WITHOUT_BILLING) as string[], - { - setSidebarSteps: (_, { steps }) => steps, - }, - ], - isDemoProject: [ - false as null | boolean, - { - setState: (_, { isDemoProject }) => isDemoProject, - }, - ], - generatingDemoData: [ - false as boolean | null, - { - setState: (_, { generatingDemoData }) => generatingDemoData, - }, - ], - pollTimeout: [ - 0, - { - setPollTimeout: (_, payload) => payload.pollTimeout, - }, - ], - isProjectSwitcherShown: [ - false, - { - toggleProjectSwitcher: (state) => !state, - hideProjectSwitcher: () => false, - }, - ], - }), - loaders(({ actions, values }) => ({ - isDemoDataReady: [ - false as boolean, - { - checkIfDemoDataIsReady: async (_, breakpoint) => { - await breakpoint(1) - - clearTimeout(values.pollTimeout) - - try { - const res = await api.get('api/projects/@current/is_generating_demo_data') - if (!res.is_generating_demo_data) { - return true - } - const pollTimeoutMilliseconds = 1000 - const timeout = window.setTimeout(actions.checkIfDemoDataIsReady, pollTimeoutMilliseconds) - actions.setPollTimeout(timeout) - return false - } catch (e) { - return false - } - }, - }, - ], - })), - selectors(() => ({ - currentState: [ - (s) => [ - s.platform, - s.framework, - s.readyToVerify, - s.showSuperpowers, - s.showBilling, - s.isTechnicalUser, - s.hasInvitedMembers, - s.isDemoProject, - s.generatingDemoData, - ], - ( - platform, - framework, - readyToVerify, - showSuperpowers, - showBilling, - isTechnicalUser, - hasInvitedMembers, - isDemoProject, - generatingDemoData - ) => ({ - platform, - framework, - readyToVerify, - showSuperpowers, - showBilling, - isTechnicalUser, - hasInvitedMembers, - isDemoProject, - generatingDemoData, - }), - ], - currentView: [ - (s) => [s.currentState], - ({ - isTechnicalUser, - platform, - framework, - readyToVerify, - showSuperpowers, - showBilling, - hasInvitedMembers, - isDemoProject, - generatingDemoData, - }) => { - if (isDemoProject) { - return INGESTION_VIEWS.NO_DEMO_INGESTION - } - if (showBilling) { - return INGESTION_VIEWS.BILLING - } - if (showSuperpowers) { - return INGESTION_VIEWS.SUPERPOWERS - } - if (readyToVerify) { - return INGESTION_VIEWS.VERIFICATION - } - if (isTechnicalUser) { - if (!platform) { - return INGESTION_VIEWS.CHOOSE_PLATFORM - } - if (framework || platform === WEB) { - return INGESTION_VIEWS.WEB_INSTRUCTIONS - } - if (platform === MOBILE || platform === BACKEND) { - return INGESTION_VIEWS.CHOOSE_FRAMEWORK - } - if (platform === THIRD_PARTY) { - return INGESTION_VIEWS.CHOOSE_THIRD_PARTY - } - // could be null, so we check that it's set to false - } else if (isTechnicalUser === false) { - if (generatingDemoData) { - return INGESTION_VIEWS.GENERATING_DEMO_DATA - } - if (hasInvitedMembers) { - return INGESTION_VIEWS.TEAM_INVITED - } - if (!platform && !readyToVerify) { - return INGESTION_VIEWS.INVITE_TEAM - } - } - return INGESTION_VIEWS.INVITE_TEAM - }, - ], - currentStep: [ - (s) => [s.currentView], - (currentView) => { - return INGESTION_VIEW_TO_STEP[currentView] - }, - ], - previousStep: [ - (s) => [s.currentStep], - (currentStep) => { - const currentStepIndex = Object.values(INGESTION_STEPS).indexOf(currentStep) - return Object.values(INGESTION_STEPS)[currentStepIndex - 1] - }, - ], - frameworkString: [ - (s) => [s.framework], - (framework): string => { - if (framework) { - const frameworkStrings = { - NODEJS: 'Node.js', - GO: 'Go', - RUBY: 'Ruby', - PYTHON: 'Python', - PHP: 'PHP', - ELIXIR: 'Elixir', - ANDROID: 'Android', - IOS: 'iOS', - REACT_NATIVE: 'React Native', - FLUTTER: 'Flutter', - API: 'HTTP API', - } - return frameworkStrings[framework] || framework - } - return '' - }, - ], - showBillingStep: [ - (s) => [s.preflight], - (preflight): boolean => { - return !!preflight?.cloud && !preflight?.demo - }, - ], - })), - - actionToUrl(({ values }) => ({ - setState: () => getUrl(values), - updateCurrentTeamSuccess: (val) => { - if ( - (router.values.location.pathname.includes( - values.showBillingStep ? '/ingestion/billing' : '/ingestion/superpowers' - ) || - router.values.location.pathname.includes('/ingestion/invites-sent')) && - val.payload?.completed_snippet_onboarding - ) { - return combineUrl(urls.events(), { onboarding_completed: true }).url - } - }, - })), - - urlToAction(({ actions, values }) => ({ - '/ingestion': () => actions.goToView(INGESTION_VIEWS.INVITE_TEAM), - '/ingestion/invites-sent': () => actions.goToView(INGESTION_VIEWS.TEAM_INVITED), - '/ingestion/superpowers': () => actions.goToView(INGESTION_VIEWS.SUPERPOWERS), - '/ingestion/billing': () => actions.goToView(INGESTION_VIEWS.BILLING), - '/ingestion/verify': () => actions.goToView(INGESTION_VIEWS.VERIFICATION), - '/ingestion/platform': () => actions.goToView(INGESTION_VIEWS.CHOOSE_FRAMEWORK), - '/ingestion(/:platform)(/:framework)': (pathParams, searchParams) => { - const platform = pathParams.platform || searchParams.platform || null - const framework = pathParams.framework || searchParams.framework || null - actions.setState({ - isTechnicalUser: true, - hasInvitedMembers: null, - platform: platform, - framework: framework, - readyToVerify: false, - showBilling: false, - showSuperpowers: false, - isDemoProject: values.isDemoProject, - generatingDemoData: false, - }) - }, - })), - listeners(({ actions, values }) => ({ - next: (props) => { - actions.setState({ ...values.currentState, ...props } as IngestionState) - }, - goToView: ({ view }) => { - actions.setState(viewToState(view, values.currentState as IngestionState)) - }, - completeOnboarding: () => { - teamLogic.actions.updateCurrentTeam({ - completed_snippet_onboarding: true, - }) - if ( - !values.currentTeam?.session_recording_opt_in || - !values.currentTeam?.capture_console_log_opt_in || - !values.currentTeam?.capture_performance_opt_in - ) { - eventUsageLogic.actions.reportIngestionRecordingsTurnedOff( - !!values.currentTeam?.session_recording_opt_in, - !!values.currentTeam?.capture_console_log_opt_in, - !!values.currentTeam?.capture_performance_opt_in - ) - } - if (values.currentTeam?.autocapture_opt_out) { - eventUsageLogic.actions.reportIngestionAutocaptureToggled(!!values.currentTeam?.autocapture_opt_out) - } - }, - setPlatform: ({ platform }) => { - eventUsageLogic.actions.reportIngestionSelectPlatformType(platform) - }, - setFramework: ({ framework }) => { - eventUsageLogic.actions.reportIngestionSelectFrameworkType(framework) - }, - sidebarStepClick: ({ step }) => { - switch (step) { - case INGESTION_STEPS.START: - actions.goToView(INGESTION_VIEWS.INVITE_TEAM) - return - case INGESTION_STEPS.PLATFORM: - actions.goToView(INGESTION_VIEWS.CHOOSE_PLATFORM) - return - case INGESTION_STEPS.CONNECT_PRODUCT: - actions.goToView(INGESTION_VIEWS.CHOOSE_FRAMEWORK) - return - case INGESTION_STEPS.VERIFY: - actions.goToView(INGESTION_VIEWS.VERIFICATION) - return - case INGESTION_STEPS.BILLING: - actions.goToView(INGESTION_VIEWS.BILLING) - return - case INGESTION_STEPS.SUPERPOWERS: - actions.goToView(INGESTION_VIEWS.SUPERPOWERS) - return - default: - return - } - }, - onBack: () => { - switch (values.currentView) { - case INGESTION_VIEWS.BILLING: - return actions.goToView(INGESTION_VIEWS.VERIFICATION) - case INGESTION_VIEWS.SUPERPOWERS: - return actions.goToView(INGESTION_VIEWS.CHOOSE_FRAMEWORK) - case INGESTION_VIEWS.TEAM_INVITED: - return actions.goToView(INGESTION_VIEWS.INVITE_TEAM) - case INGESTION_VIEWS.CHOOSE_PLATFORM: - return actions.goToView(INGESTION_VIEWS.INVITE_TEAM) - case INGESTION_VIEWS.VERIFICATION: - return actions.goToView(INGESTION_VIEWS.SUPERPOWERS) - case INGESTION_VIEWS.WEB_INSTRUCTIONS: - return actions.goToView(INGESTION_VIEWS.CHOOSE_PLATFORM) - case INGESTION_VIEWS.CHOOSE_FRAMEWORK: - return actions.goToView(INGESTION_VIEWS.CHOOSE_PLATFORM) - // If they're on the InviteTeam step, but on the Team Invited panel, - // we still want them to be able to go back to the previous step. - // So this resets the state for that panel so they can go back. - case INGESTION_VIEWS.INVITE_TEAM: - return actions.goToView(INGESTION_VIEWS.INVITE_TEAM) - case INGESTION_VIEWS.CHOOSE_THIRD_PARTY: - return actions.goToView(INGESTION_VIEWS.CHOOSE_PLATFORM) - default: - return actions.goToView(INGESTION_VIEWS.INVITE_TEAM) - } - }, - inviteTeamMembersSuccess: () => { - if (router.values.location.pathname.includes(urls.ingestion())) { - actions.setState(viewToState(INGESTION_VIEWS.TEAM_INVITED, values.currentState as IngestionState)) - } - }, - createTeamSuccess: ({ currentTeam }) => { - if (window.location.href.includes(urls.ingestion()) && currentTeam.is_demo) { - actions.checkIfDemoDataIsReady(null) - } else { - window.location.href = urls.ingestion() - } - }, - checkIfDemoDataIsReadySuccess: ({ isDemoDataReady }) => { - if (isDemoDataReady) { - window.location.href = urls.default() - } - }, - })), - subscriptions(({ actions, values }) => ({ - showBillingStep: (value) => { - const steps = value ? INGESTION_STEPS : INGESTION_STEPS_WITHOUT_BILLING - actions.setSidebarSteps(Object.values(steps)) - }, - currentTeam: (currentTeam: TeamType) => { - if (currentTeam?.ingested_event && values.readyToVerify && !values.showBillingStep) { - actions.setCurrentStep(INGESTION_STEPS.DONE) - } - }, - })), -]) - -function getUrl(values: ingestionLogicType['values']): string | [string, Record] { - const { - isTechnicalUser, - platform, - framework, - readyToVerify, - showBilling, - showSuperpowers, - hasInvitedMembers, - generatingDemoData, - } = values - - let url = '/ingestion' - - if (showBilling) { - return url + '/billing' - } - - if (showSuperpowers) { - url += '/superpowers' - return [ - url, - { - platform: platform || undefined, - framework: framework?.toLowerCase() || undefined, - }, - ] - } - - if (readyToVerify) { - url += '/verify' - return [ - url, - { - platform: platform || undefined, - framework: framework?.toLowerCase() || undefined, - }, - ] - } - - if (isTechnicalUser) { - if (framework === API) { - url += '/api' - return [ - url, - { - platform: platform || undefined, - }, - ] - } - - if (platform === MOBILE) { - url += '/mobile' - } - - if (platform === WEB) { - url += '/web' - } - - if (platform === BACKEND) { - url += '/backend' - } - - if (generatingDemoData) { - url += '/just-exploring' - } - - if (platform === THIRD_PARTY) { - url += '/third-party' - } - - if (!platform) { - url += '/platform' - } - - if (framework) { - url += `/${framework.toLowerCase()}` - } - } else { - if (!platform && hasInvitedMembers) { - url += '/invites-sent' - } - } - - return url -} diff --git a/frontend/src/scenes/ingestion/panels/BillingPanel.tsx b/frontend/src/scenes/ingestion/panels/BillingPanel.tsx deleted file mode 100644 index 97e8388d2d1fb..0000000000000 --- a/frontend/src/scenes/ingestion/panels/BillingPanel.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import './Panels.scss' - -import { LemonDivider } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { Billing } from 'scenes/billing/Billing' -import { billingLogic } from 'scenes/billing/billingLogic' -import { CardContainer } from 'scenes/ingestion/CardContainer' -import { ingestionLogic } from 'scenes/ingestion/ingestionLogic' - -export function BillingPanel(): JSX.Element { - const { completeOnboarding } = useActions(ingestionLogic) - const { reportIngestionContinueWithoutBilling } = useActions(eventUsageLogic) - const { billing } = useValues(billingLogic) - - if (!billing) { - return ( - -
- - - -
-
- - -
- - ) - } - - const hasSubscribedToAllProducts = billing.products - .filter((product) => !product.contact_support) - .every((product) => product.subscribed) - const hasSubscribedToAnyProduct = billing.products.some((product) => product.subscribed) - - return ( - - {hasSubscribedToAllProducts ? ( -
-

You're good to go!

- -

- Your organisation is setup for billing with premium features and the increased free tiers - enabled. -

- { - completeOnboarding() - }} - > - Complete - -
- ) : ( -
-

Subscribe for access to all features

- - - - - { - completeOnboarding() - !hasSubscribedToAnyProduct && reportIngestionContinueWithoutBilling() - }} - > - {hasSubscribedToAnyProduct ? 'Continue' : 'Skip for now'} - -
- )} -
- ) -} diff --git a/frontend/src/scenes/ingestion/panels/FrameworkPanel.tsx b/frontend/src/scenes/ingestion/panels/FrameworkPanel.tsx deleted file mode 100644 index 96885ff55084a..0000000000000 --- a/frontend/src/scenes/ingestion/panels/FrameworkPanel.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import './Panels.scss' - -import { useActions, useValues } from 'kea' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { CardContainer } from 'scenes/ingestion/CardContainer' -import { API, BACKEND, mobileFrameworks, webFrameworks } from 'scenes/ingestion/constants' - -import { IngestionInviteMembersButton } from '../IngestionInviteMembersButton' -import { ingestionLogic } from '../ingestionLogic' - -export function FrameworkPanel(): JSX.Element { - const { next } = useActions(ingestionLogic) - const { platform } = useValues(ingestionLogic) - const frameworks = platform === BACKEND ? webFrameworks : mobileFrameworks - - return ( - -
-

- {platform === BACKEND ? 'Choose the framework your app is built in' : 'Pick a mobile platform'} -

-

- We'll provide you with snippets that you can easily add to your codebase to get started! -

-
- {(Object.keys(frameworks) as (keyof typeof frameworks)[]).map((item) => ( - next({ framework: item })} - > - {frameworks[item]} - - ))} - next({ framework: API })} - > - Other - - -
-
-
- ) -} diff --git a/frontend/src/scenes/ingestion/panels/GeneratingDemoDataPanel.tsx b/frontend/src/scenes/ingestion/panels/GeneratingDemoDataPanel.tsx deleted file mode 100644 index ade270cdcd856..0000000000000 --- a/frontend/src/scenes/ingestion/panels/GeneratingDemoDataPanel.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import './Panels.scss' - -import { useValues } from 'kea' -import { LemonBanner } from 'lib/lemon-ui/LemonBanner' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { organizationLogic } from 'scenes/organizationLogic' - -import { CardContainer } from '../CardContainer' - -export function GeneratingDemoDataPanel(): JSX.Element { - const { currentOrganization } = useValues(organizationLogic) - return ( - -
-
-
- -
-

Generating demo data...

-

- Your demo data is on the way! This can take up to one minute - we'll redirect you when your demo - data is ready. -

- - We're using a demo project. Your other {currentOrganization?.name} projects won't be - affected. - -
-
-
- ) -} diff --git a/frontend/src/scenes/ingestion/panels/InstructionsPanel.scss b/frontend/src/scenes/ingestion/panels/InstructionsPanel.scss deleted file mode 100644 index 5d7b1c7ce4408..0000000000000 --- a/frontend/src/scenes/ingestion/panels/InstructionsPanel.scss +++ /dev/null @@ -1,30 +0,0 @@ -.InstructionsPanel { - max-width: 50rem; - - h1 { - font-size: 28px; - font-weight: 800; - line-height: 40px; - letter-spacing: -0.02em; - } - - h2 { - font-size: 20px; - font-weight: 800; - line-height: 24px; - letter-spacing: -0.02em; - margin-top: 0.5rem; - } - - h3 { - font-size: 16px; - font-weight: 700; - line-height: 24px; - letter-spacing: 0; - margin-top: 0.5rem; - } - - ol { - padding-left: 1rem; - } -} diff --git a/frontend/src/scenes/ingestion/panels/InstructionsPanel.tsx b/frontend/src/scenes/ingestion/panels/InstructionsPanel.tsx deleted file mode 100644 index df89b805dc83c..0000000000000 --- a/frontend/src/scenes/ingestion/panels/InstructionsPanel.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import './InstructionsPanel.scss' - -import { Link } from '@posthog/lemon-ui' -import { useValues } from 'kea' -import { CardContainer } from 'scenes/ingestion/CardContainer' -import { - AndroidInstructions, - APIInstructions, - ElixirInstructions, - FlutterInstructions, - GoInstructions, - IOSInstructions, - NodeInstructions, - PHPInstructions, - PythonInstructions, - RNInstructions, - RubyInstructions, -} from 'scenes/ingestion/frameworks' - -import { API, BACKEND, MOBILE, WEB } from '../constants' -import { WebInstructions } from '../frameworks/WebInstructions' -import { ingestionLogic } from '../ingestionLogic' - -const frameworksSnippet: Record = { - NODEJS: NodeInstructions, - GO: GoInstructions, - RUBY: RubyInstructions, - PYTHON: PythonInstructions, - PHP: PHPInstructions, - ELIXIR: ElixirInstructions, - ANDROID: AndroidInstructions, - IOS: IOSInstructions, - REACT_NATIVE: RNInstructions, - FLUTTER: FlutterInstructions, - API: APIInstructions, -} - -export function InstructionsPanel(): JSX.Element { - const { platform, framework, frameworkString } = useValues(ingestionLogic) - - if (platform !== WEB && !framework) { - return <> - } - - const FrameworkSnippet: React.ComponentType = frameworksSnippet[framework as string] as React.ComponentType - - return ( -
- {platform === WEB ? ( - - - - ) : framework === API ? ( - -

{frameworkString}

-

- Need a different framework? Our HTTP API is a flexible way to use PostHog anywhere. Try the - endpoint below to send your first event, and view our API docs{' '} - here. -

- -
- ) : ( - -

{`Setup ${frameworkString}`}

- - {platform === BACKEND ? ( - <> -

- Follow the instructions below to send custom events from your {frameworkString} backend. -

- - - ) : null} - {platform === MOBILE ? : null} -
- )} -
- ) -} diff --git a/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx b/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx deleted file mode 100644 index 6ab8b99020641..0000000000000 --- a/frontend/src/scenes/ingestion/panels/InviteTeamPanel.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import './Panels.scss' - -import { useActions } from 'kea' -import { IconChevronRight } from 'lib/lemon-ui/icons' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { ingestionLogic } from 'scenes/ingestion/ingestionLogic' -import { inviteLogic } from 'scenes/settings/organization/inviteLogic' - -import { DemoProjectButton } from './PanelComponents' - -export function InviteTeamPanel(): JSX.Element { - const { next } = useActions(ingestionLogic) - const { showInviteModal } = useActions(inviteLogic) - const { reportInviteMembersButtonClicked } = useActions(eventUsageLogic) - - return ( -
-

Welcome to PostHog

-

- PostHog enables you to understand your customers, answer product questions, and test new features{' '} - - all in our comprehensive product suite. To get started, we'll need to add a code snippet to your - product. -

- -
- next({ isTechnicalUser: true })} - fullWidth - size="large" - className="mb-4" - type="primary" - sideIcon={} - > -
-

I can add a code snippet to my product.

-

- Available for JavaScript, Android, iOS, React Native, Node.js, Ruby, Go, and more. -

-
-
- { - showInviteModal() - reportInviteMembersButtonClicked() - }} - fullWidth - size="large" - className="mb-4" - type="secondary" - sideIcon={} - > -
-

I'll need a team member to add the code snippet to our product.

-

- We'll send an invite and instructions for getting the code snippet added. -

-
-
- -
-
- ) -} diff --git a/frontend/src/scenes/ingestion/panels/NoDemoIngestionPanel.tsx b/frontend/src/scenes/ingestion/panels/NoDemoIngestionPanel.tsx deleted file mode 100644 index 6c4acb89f0ae6..0000000000000 --- a/frontend/src/scenes/ingestion/panels/NoDemoIngestionPanel.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import './Panels.scss' - -import { LemonButton } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { IconArrowRight } from 'lib/lemon-ui/icons' -import { organizationLogic } from 'scenes/organizationLogic' -import { userLogic } from 'scenes/userLogic' - -import { CardContainer } from '../CardContainer' - -export function NoDemoIngestionPanel(): JSX.Element { - const { currentOrganization } = useValues(organizationLogic) - const { updateCurrentTeam } = useActions(userLogic) - - return ( - -
-

Whoops!

-

- New events can't be ingested into a demo project. But, you can switch to another project if you'd - like: -

-
- {currentOrganization?.teams - ?.filter((team) => !team.is_demo) - .map((team) => ( -

- } - fullWidth - onClick={() => updateCurrentTeam(team.id)} - > - {team.name} - -

- ))} -
-
-
- ) -} diff --git a/frontend/src/scenes/ingestion/panels/PanelComponents.tsx b/frontend/src/scenes/ingestion/panels/PanelComponents.tsx deleted file mode 100644 index a1f053b4fa235..0000000000000 --- a/frontend/src/scenes/ingestion/panels/PanelComponents.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import './Panels.scss' - -import { useActions, useValues } from 'kea' -import { FEATURE_FLAGS } from 'lib/constants' -import { IconArrowLeft, IconChevronRight } from 'lib/lemon-ui/icons' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { organizationLogic } from 'scenes/organizationLogic' -import { teamLogic } from 'scenes/teamLogic' -import { userLogic } from 'scenes/userLogic' - -import { IngestionInviteMembersButton } from '../IngestionInviteMembersButton' -import { INGESTION_STEPS, ingestionLogic, IngestionState } from '../ingestionLogic' - -const DEMO_TEAM_NAME: string = 'Hedgebox' - -export function PanelFooter({ - nextProps, - onContinue, - finalStep = false, - showInviteTeamMembers = true, -}: { - nextProps: Partial - onContinue?: () => void - finalStep?: boolean - showInviteTeamMembers?: boolean -}): JSX.Element { - const { next } = useActions(ingestionLogic) - - return ( -
- -
- { - onContinue && onContinue() - next(nextProps) - }} - > - {finalStep ? 'Complete' : 'Continue'} - - {showInviteTeamMembers && } -
-
- ) -} - -export function PanelHeader(): JSX.Element | null { - const { isSmallScreen, previousStep, currentStep, hasInvitedMembers } = useValues(ingestionLogic) - const { onBack } = useActions(ingestionLogic) - - // no back buttons on the Getting Started step - // but only if it's not the MembersInvited panel - // (since they'd want to be able to go back from there) - if (currentStep === INGESTION_STEPS.START && !hasInvitedMembers) { - return null - } - - return ( -
- } size="small"> - {isSmallScreen - ? '' - : // If we're on the MembersInvited panel, they "go back" to - // the Get Started step, even though it's technically the same step - currentStep === INGESTION_STEPS.START && hasInvitedMembers - ? currentStep - : previousStep} - -
- ) -} - -export function DemoProjectButton({ text, subtext }: { text: string; subtext?: string }): JSX.Element { - const { next } = useActions(ingestionLogic) - const { createTeam } = useActions(teamLogic) - const { currentOrganization } = useValues(organizationLogic) - const { updateCurrentTeam } = useActions(userLogic) - const { reportIngestionTryWithDemoDataClicked, reportProjectCreationSubmitted } = useActions(eventUsageLogic) - const { featureFlags } = useValues(featureFlagLogic) - - if (featureFlags[FEATURE_FLAGS.ONBOARDING_V2_DEMO] !== 'test') { - return <> - } - return ( - { - // If the current org has a demo team, just navigate there - if (currentOrganization?.teams && currentOrganization.teams.filter((team) => team.is_demo).length > 0) { - updateCurrentTeam(currentOrganization.teams.filter((team) => team.is_demo)[0].id) - } else { - // Create a new demo team - createTeam({ name: DEMO_TEAM_NAME, is_demo: true }) - next({ isTechnicalUser: false, generatingDemoData: true }) - reportProjectCreationSubmitted( - currentOrganization?.teams ? currentOrganization.teams.length : 0, - DEMO_TEAM_NAME.length - ) - } - reportIngestionTryWithDemoDataClicked() - }} - fullWidth - size="large" - className="ingestion-view-demo-data mb-4" - type="secondary" - sideIcon={} - > -
-

- {currentOrganization?.teams && currentOrganization.teams.filter((team) => team.is_demo).length > 0 - ? 'Explore the demo project' - : text} -

- {subtext ?

{subtext}

: null} -
-
- ) -} diff --git a/frontend/src/scenes/ingestion/panels/Panels.scss b/frontend/src/scenes/ingestion/panels/Panels.scss deleted file mode 100644 index ca98aa806c405..0000000000000 --- a/frontend/src/scenes/ingestion/panels/Panels.scss +++ /dev/null @@ -1,37 +0,0 @@ -.FrameworkPanel { - max-width: 400px; -} - -.panel-footer { - background-color: white; - margin-bottom: 1rem; - bottom: 0; -} - -.ingestion-title { - font-size: 28px; - font-weight: 700; - line-height: 40px; - display: flex; - align-items: center; - gap: 0.5rem; - margin: 0; -} - -.IngestionSubtitle { - font-size: 20px; - font-weight: 800; - margin: 1rem 0; -} - -.prompt-text { - margin-top: 1rem; -} - -.ingestion-listening-for-events { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - margin-bottom: 1rem; -} diff --git a/frontend/src/scenes/ingestion/panels/PlatformPanel.tsx b/frontend/src/scenes/ingestion/panels/PlatformPanel.tsx deleted file mode 100644 index e7de3f5c6aeee..0000000000000 --- a/frontend/src/scenes/ingestion/panels/PlatformPanel.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import './Panels.scss' - -import { useActions } from 'kea' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' - -import { platforms, THIRD_PARTY } from '../constants' -import { IngestionInviteMembersButton } from '../IngestionInviteMembersButton' -import { ingestionLogic } from '../ingestionLogic' - -export function PlatformPanel(): JSX.Element { - const { next } = useActions(ingestionLogic) - - return ( -
-

Where do you want to send events from?

-

- With PostHog, you can collect events from nearly anywhere. Select one to start, and you can always add - more sources later. -

- -
- {platforms.map((platform) => ( - next({ platform })} - > - {platform} - - ))} - next({ platform: THIRD_PARTY })} - fullWidth - center - size="large" - className="mb-2" - type="primary" - > - Import events from a third party - - -
-
- ) -} diff --git a/frontend/src/scenes/ingestion/panels/SuperpowersPanel.tsx b/frontend/src/scenes/ingestion/panels/SuperpowersPanel.tsx deleted file mode 100644 index 4f92f763196c4..0000000000000 --- a/frontend/src/scenes/ingestion/panels/SuperpowersPanel.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { LemonSwitch, Link } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { SupportHeroHog } from 'lib/components/hedgehogs' -import { useState } from 'react' -import { CardContainer } from 'scenes/ingestion/CardContainer' -import { teamLogic } from 'scenes/teamLogic' - -import { ingestionLogic } from '../ingestionLogic' - -export function SuperpowersPanel(): JSX.Element { - const { updateCurrentTeam } = useActions(teamLogic) - const { showBillingStep } = useValues(ingestionLogic) - const { completeOnboarding } = useActions(ingestionLogic) - const [sessionRecordingsChecked, setSessionRecordingsChecked] = useState(true) - const [autocaptureChecked, setAutocaptureChecked] = useState(true) - - return ( - { - updateCurrentTeam({ - session_recording_opt_in: sessionRecordingsChecked, - capture_console_log_opt_in: sessionRecordingsChecked, - capture_performance_opt_in: sessionRecordingsChecked, - autocapture_opt_out: !autocaptureChecked, - }) - if (!showBillingStep) { - completeOnboarding() - } - }} - finalStep={!showBillingStep} - > -
-
-

Enable your product superpowers

-

- Collecting events from your app is just the first step toward building great products. PostHog - gives you other superpowers, too, like recording user sessions and automagically capturing - frontend interactions. -

-
-
- -
-
-
- { - setSessionRecordingsChecked(checked) - }} - label="Record user sessions" - fullWidth={true} - labelClassName={'text-base font-semibold'} - checked={sessionRecordingsChecked} - /> -

- See recordings of how your users are really using your product with powerful features like error - tracking, filtering, and analytics.{' '} - - Learn more - {' '} - about Session recordings. -

-
-
- { - setAutocaptureChecked(checked) - }} - label="Autocapture frontend interactions" - fullWidth={true} - labelClassName={'text-base font-semibold'} - checked={autocaptureChecked} - /> -

- If you use our JavaScript or React Native libraries, we'll automagically capture frontend - interactions like pageviews, clicks, and more.{' '} - - Fine-tune what you capture - {' '} - directly in your code snippet. -

-
-
- ) -} diff --git a/frontend/src/scenes/ingestion/panels/TeamInvitedPanel.tsx b/frontend/src/scenes/ingestion/panels/TeamInvitedPanel.tsx deleted file mode 100644 index dc866c848bb96..0000000000000 --- a/frontend/src/scenes/ingestion/panels/TeamInvitedPanel.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import './Panels.scss' - -import { useActions } from 'kea' -import { IconChevronRight } from 'lib/lemon-ui/icons' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonDivider } from 'lib/lemon-ui/LemonDivider' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { ingestionLogic } from 'scenes/ingestion/ingestionLogic' - -import { DemoProjectButton } from './PanelComponents' - -export function TeamInvitedPanel(): JSX.Element { - const { completeOnboarding } = useActions(ingestionLogic) - const { reportIngestionContinueWithoutVerifying } = useActions(eventUsageLogic) - - return ( -
-

Help is on the way!

-

You can still explore PostHog while you wait for your team members to join.

- -
- - { - completeOnboarding() - reportIngestionContinueWithoutVerifying() - }} - fullWidth - size="large" - className="mb-4" - type="secondary" - sideIcon={} - > -
-

Continue without any events.

-

- It might look a little empty in there, but we'll do our best. -

-
-
-
-
- ) -} diff --git a/frontend/src/scenes/ingestion/panels/ThirdPartyIcons.tsx b/frontend/src/scenes/ingestion/panels/ThirdPartyIcons.tsx deleted file mode 100644 index 1ebb0e6545346..0000000000000 --- a/frontend/src/scenes/ingestion/panels/ThirdPartyIcons.tsx +++ /dev/null @@ -1,58 +0,0 @@ -export const Segment = (props: React.SVGProps): JSX.Element => { - return ( - - - - - - - - - - - - - ) -} - -export const RSS = (props: React.SVGProps): JSX.Element => { - return ( - - - - - - - - - - - - - - - - - - - - ) -} diff --git a/frontend/src/scenes/ingestion/panels/ThirdPartyPanel.tsx b/frontend/src/scenes/ingestion/panels/ThirdPartyPanel.tsx deleted file mode 100644 index edfe3b6607aa2..0000000000000 --- a/frontend/src/scenes/ingestion/panels/ThirdPartyPanel.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import './Panels.scss' - -import { Link } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { CodeSnippet } from 'lib/components/CodeSnippet' -import { IconOpenInNew } from 'lib/lemon-ui/icons' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { LemonModal } from 'lib/lemon-ui/LemonModal' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { teamLogic } from 'scenes/teamLogic' - -import { CardContainer } from '../CardContainer' -import { thirdPartySources } from '../constants' -import { ingestionLogic } from '../ingestionLogic' - -export function ThirdPartyPanel(): JSX.Element { - const { setInstructionsModal, setThirdPartySource } = useActions(ingestionLogic) - const { reportIngestionThirdPartyAboutClicked, reportIngestionThirdPartyConfigureClicked } = - useActions(eventUsageLogic) - - return ( - -
-

Set up third-party integrations

- {thirdPartySources.map((source, idx) => { - return ( -
-
-
-
{source.icon}
-
-

- {source.name} Import - {source.labels?.map((label, labelIdx) => ( - - {label} - - ))} -

-

- {source.description - ? source.description - : `Send events from ${source.name} into PostHog`} -

-
-
-
- { - reportIngestionThirdPartyAboutClicked(source.name) - }} - > - About - - { - setThirdPartySource(idx) - setInstructionsModal(true) - reportIngestionThirdPartyConfigureClicked(source.name) - }} - > - Configure - -
-
-
- ) - })} -
- -
- ) -} - -export function IntegrationInstructionsModal(): JSX.Element { - const { instructionsModalOpen, thirdPartyIntegrationSource } = useValues(ingestionLogic) - const { setInstructionsModal } = useActions(ingestionLogic) - const { currentTeam } = useValues(teamLogic) - - return ( - <> - {thirdPartyIntegrationSource?.name && ( - setInstructionsModal(false)} - title="Configure integration" - footer={ - setInstructionsModal(false)}> - Done - - } - > -
-

- {thirdPartyIntegrationSource.icon} - Integrate with {thirdPartyIntegrationSource.name} -

-
-
-

- The{' '} - - {thirdPartyIntegrationSource.name} docs page for the PostHog integration - {' '} - provides a detailed overview of how to set up this integration. -

- PostHog Project API Key - {currentTeam?.api_token || ''} -
-
- window.open(thirdPartyIntegrationSource.aboutLink)} - sideIcon={} - > - Take me to the {thirdPartyIntegrationSource.name} docs - -
-

Steps:

-
    -
  1. Complete the steps for the {thirdPartyIntegrationSource.name} integration.
  2. -
  3. - Close this step and click continue to begin listening for events. -
  4. -
-
-
-
- )} - - ) -} diff --git a/frontend/src/scenes/ingestion/panels/VerificationPanel.tsx b/frontend/src/scenes/ingestion/panels/VerificationPanel.tsx deleted file mode 100644 index 71a0356690cf3..0000000000000 --- a/frontend/src/scenes/ingestion/panels/VerificationPanel.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import './Panels.scss' - -import { useActions, useValues } from 'kea' -import { useInterval } from 'lib/hooks/useInterval' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { Spinner } from 'lib/lemon-ui/Spinner/Spinner' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { teamLogic } from 'scenes/teamLogic' - -import { CardContainer } from '../CardContainer' -import { IngestionInviteMembersButton } from '../IngestionInviteMembersButton' -import { ingestionLogic } from '../ingestionLogic' - -export function VerificationPanel(): JSX.Element { - const { loadCurrentTeam } = useActions(teamLogic) - const { currentTeam } = useValues(teamLogic) - const { next } = useActions(ingestionLogic) - const { reportIngestionContinueWithoutVerifying } = useActions(eventUsageLogic) - - useInterval(() => { - if (!currentTeam?.ingested_event) { - loadCurrentTeam() - } - }, 2000) - - return !currentTeam?.ingested_event ? ( - -
-
- -

Listening for events...

-

- Once you have integrated the snippet and sent an event, we will verify it was properly received - and continue. -

- - { - next({ showSuperpowers: true }) - reportIngestionContinueWithoutVerifying() - }} - > - or continue without verifying - -
-
-
- ) : ( - -
-
-

Successfully sent events!

-

- You will now be able to explore PostHog and take advantage of all its features to understand - your users. -

-
-
-
- ) -} diff --git a/frontend/src/scenes/ingestion/types.ts b/frontend/src/scenes/ingestion/types.ts deleted file mode 100644 index ce40713338cc5..0000000000000 --- a/frontend/src/scenes/ingestion/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { API, BACKEND, MOBILE, mobileFrameworks, THIRD_PARTY, WEB, webFrameworks } from 'scenes/ingestion/constants' - -export type Framework = keyof typeof webFrameworks | keyof typeof mobileFrameworks | typeof API | null - -export type PlatformType = typeof WEB | typeof MOBILE | typeof BACKEND | typeof THIRD_PARTY | null diff --git a/frontend/src/scenes/insights/InsightPageHeader.tsx b/frontend/src/scenes/insights/InsightPageHeader.tsx index cf0bbd8320f58..3e6ff65488817 100644 --- a/frontend/src/scenes/insights/InsightPageHeader.tsx +++ b/frontend/src/scenes/insights/InsightPageHeader.tsx @@ -256,7 +256,7 @@ export function InsightPageHeader({ insightLogicProps }: { insightLogicProps: In - deleteWithUndo({ + void deleteWithUndo({ object: insight, endpoint: `projects/${currentTeamId}/insights`, callback: () => { diff --git a/frontend/src/scenes/insights/aggregationAxisFormats.test.ts b/frontend/src/scenes/insights/aggregationAxisFormats.test.ts index d487b233ae2ea..56db792c0a3e3 100644 --- a/frontend/src/scenes/insights/aggregationAxisFormats.test.ts +++ b/frontend/src/scenes/insights/aggregationAxisFormats.test.ts @@ -26,7 +26,9 @@ describe('formatAggregationAxisValue', () => { }, ] formatTestcases.forEach((testcase) => { - it(`correctly formats "${testcase.candidate}" as ${testcase.expected} when filters are ${testcase.filters}`, () => { + it(`correctly formats "${testcase.candidate}" as ${testcase.expected} when filters are ${JSON.stringify( + testcase.filters + )}`, () => { expect(formatAggregationAxisValue(testcase.filters as Partial, testcase.candidate)).toEqual( testcase.expected ) diff --git a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts index 0ea2ebb7f1f05..b2e47f5785b6e 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts +++ b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts @@ -115,7 +115,7 @@ export const entityFilterLogic = kea([ }, ], localFilters: [ - toLocalFilters(props.filters ?? {}) as LocalFilter[], + toLocalFilters(props.filters ?? {}), { setLocalFilters: (_, { filters }) => toLocalFilters(filters), }, @@ -178,9 +178,7 @@ export const entityFilterLogic = kea([ }, updateFilterProperty: async ({ properties, index }) => { actions.setFilters( - values.localFilters.map( - (filter, i) => (i === index ? { ...filter, properties } : filter) as LocalFilter - ) + values.localFilters.map((filter, i) => (i === index ? { ...filter, properties } : filter)) ) }, updateFilterMath: async ({ index, ...mathProperties }) => { diff --git a/frontend/src/scenes/insights/insightDataTimingLogic.ts b/frontend/src/scenes/insights/insightDataTimingLogic.ts index 3e1d2174d1b8c..22d6558d43b42 100644 --- a/frontend/src/scenes/insights/insightDataTimingLogic.ts +++ b/frontend/src/scenes/insights/insightDataTimingLogic.ts @@ -52,8 +52,7 @@ export const insightDataTimingLogic = kea([ } const duration = performance.now() - values.queryStartTimes[payload.queryId] - - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { type: 'insight_load', context: 'insight', primary_interaction_id: payload.queryId, @@ -67,6 +66,7 @@ export const insightDataTimingLogic = kea([ insight: values.query.kind, is_primary_interaction: true, }) + actions.removeQuery(payload.queryId) }, loadDataFailure: ({ errorObject }) => { @@ -76,8 +76,7 @@ export const insightDataTimingLogic = kea([ } const duration = performance.now() - values.queryStartTimes[errorObject.queryId] - - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { type: 'insight_load', context: 'insight', primary_interaction_id: errorObject.queryId, @@ -91,12 +90,12 @@ export const insightDataTimingLogic = kea([ insight: values.query.kind, is_primary_interaction: true, }) + actions.removeQuery(errorObject.queryId) }, loadDataCancellation: (payload) => { const duration = performance.now() - values.queryStartTimes[payload.queryId] - - captureTimeToSeeData(values.currentTeamId, { + void captureTimeToSeeData(values.currentTeamId, { type: 'insight_load', context: 'insight', primary_interaction_id: payload.queryId, @@ -108,6 +107,7 @@ export const insightDataTimingLogic = kea([ api_response_bytes: 0, insight: values.query.kind, }) + actions.removeQuery(payload.queryId) }, })), diff --git a/frontend/src/scenes/insights/insightSceneLogic.test.ts b/frontend/src/scenes/insights/insightSceneLogic.test.ts index 1395d7843cb10..1d3b68fa2300d 100644 --- a/frontend/src/scenes/insights/insightSceneLogic.test.ts +++ b/frontend/src/scenes/insights/insightSceneLogic.test.ts @@ -50,7 +50,7 @@ describe('insightSceneLogic', () => { location: partial({ pathname: urls.insightNew(), search: '', hash: '' }), }) - await expect(logic.values.insightLogicRef?.logic.values.filters.insight).toEqual(InsightType.FUNNELS) + expect(logic.values.insightLogicRef?.logic.values.filters.insight).toEqual(InsightType.FUNNELS) }) it('persists edit mode in the url', async () => { diff --git a/frontend/src/scenes/insights/insightVizDataLogic.ts b/frontend/src/scenes/insights/insightVizDataLogic.ts index c71583de53b5d..a2f469e3c5b59 100644 --- a/frontend/src/scenes/insights/insightVizDataLogic.ts +++ b/frontend/src/scenes/insights/insightVizDataLogic.ts @@ -35,7 +35,6 @@ import { FunnelsQuery, InsightFilter, InsightQueryNode, - InsightVizNode, Node, NodeKind, TrendsFilter, @@ -343,7 +342,7 @@ export const insightVizDataLogic = kea([ setQuery: ({ query }) => { if (isInsightVizNode(query)) { if (props.setQuery) { - props.setQuery(query as InsightVizNode) + props.setQuery(query) } const querySource = query.source diff --git a/frontend/src/scenes/insights/sharedUtils.ts b/frontend/src/scenes/insights/sharedUtils.ts index bac75767cb44d..7dcc6e3c105c4 100644 --- a/frontend/src/scenes/insights/sharedUtils.ts +++ b/frontend/src/scenes/insights/sharedUtils.ts @@ -45,7 +45,7 @@ export function filterTrendsClientSideParams( return newFilters } -export function isTrendsInsight(insight?: InsightType | InsightType): boolean { +export function isTrendsInsight(insight?: InsightType): boolean { return insight === InsightType.TRENDS || insight === InsightType.LIFECYCLE || insight === InsightType.STICKINESS } diff --git a/frontend/src/scenes/insights/summarizeInsight.ts b/frontend/src/scenes/insights/summarizeInsight.ts index c6be76d8b0273..a0f4aca384486 100644 --- a/frontend/src/scenes/insights/summarizeInsight.ts +++ b/frontend/src/scenes/insights/summarizeInsight.ts @@ -22,7 +22,7 @@ import { mathsLogicType } from 'scenes/trends/mathsLogicType' import { cohortsModelType } from '~/models/cohortsModelType' import { groupsModelType } from '~/models/groupsModelType' import { extractExpressionComment } from '~/queries/nodes/DataTable/utils' -import { BreakdownFilter, InsightQueryNode, Node, StickinessQuery } from '~/queries/schema' +import { BreakdownFilter, InsightQueryNode, Node } from '~/queries/schema' import { isDataTableNode, isEventsQuery, @@ -273,7 +273,7 @@ function summarizeInsightQuery(query: InsightQueryNode, context: SummaryContext) return summary } else if (isStickinessQuery(query)) { return capitalizeFirstLetter( - (query as StickinessQuery).series + query.series .map((s) => { const actor = context.aggregationLabel(s.math_group_type_index, true).singular return `${actor} stickiness based on ${getDisplayNameFromEntityNode(s)}` diff --git a/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx b/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx index ed67b79846c47..a2df9d1b1ce37 100644 --- a/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx +++ b/frontend/src/scenes/insights/views/LineGraph/LineGraph.tsx @@ -2,6 +2,7 @@ import 'chartjs-adapter-dayjs-3' import ChartDataLabels from 'chartjs-plugin-datalabels' import ChartjsPluginStacked100, { ExtendedChartData } from 'chartjs-plugin-stacked100' +import clsx from 'clsx' import { useValues } from 'kea' import { ActiveElement, @@ -191,7 +192,7 @@ function createPinstripePattern(color: string): CanvasPattern { const canvas = document.createElement('canvas') canvas.width = 1 canvas.height = stripeWidth * 2 - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const ctx = canvas.getContext('2d')! // fill the canvas with given color @@ -203,7 +204,7 @@ function createPinstripePattern(color: string): CanvasPattern { ctx.fillRect(0, stripeWidth, 1, 2 * stripeWidth) // create a canvas pattern and rotate it - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const pattern = ctx.createPattern(canvas, 'repeat')! const xAx = Math.cos(stripeAngle) const xAy = Math.sin(stripeAngle) @@ -727,7 +728,10 @@ export function LineGraph_({ }, [datasets, hiddenLegendKeys, isDarkModeOn, trendsFilter, formula, showValueOnSeries, showPercentStackView]) return ( -
+
{showAnnotations && myLineChart && chartWidth && chartHeight ? ( ([ ], }), events(({ actions, values }) => ({ - afterMount: async () => { + afterMount: () => { if (values.featureFlags[FEATURE_FLAGS.FUNNELS_CUE_OPT_OUT]) { actions.setPermanentOptOut() } diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx index 85a9e9020fc6c..0670f89afe189 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeBacklink.tsx @@ -44,7 +44,7 @@ const Component = (props: NodeViewProps): JSX.Element => { > openNotebook(shortId, NotebookTarget.Popover)} + onClick={() => void openNotebook(shortId, NotebookTarget.Popover)} target={undefined} className="space-x-1" > diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx index acfc06eaeee85..7a70da6c5ac00 100644 --- a/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeEarlyAccessFeature.tsx @@ -133,7 +133,7 @@ export const NotebookNodeEarlyAccessFeature = createPostHogWidgetNode { - return { id: match[1] as EarlyAccessFeatureLogicProps['id'] } + return { id: match[1] } }, }, }) diff --git a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts index d61e67589a395..06326c5610c9d 100644 --- a/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts +++ b/frontend/src/scenes/notebooks/Nodes/notebookNodeLogic.ts @@ -267,7 +267,7 @@ export const notebookNodeLogic = kea([ }, })), - afterMount(async (logic) => { + afterMount((logic) => { const { props, actions, values } = logic props.notebookLogic.actions.registerNodeLogic(values.nodeId, logic as any) diff --git a/frontend/src/scenes/notebooks/Nodes/utils.tsx b/frontend/src/scenes/notebooks/Nodes/utils.tsx index 46b65eda1fd50..560989a2347bc 100644 --- a/frontend/src/scenes/notebooks/Nodes/utils.tsx +++ b/frontend/src/scenes/notebooks/Nodes/utils.tsx @@ -29,14 +29,13 @@ export function posthogNodePasteRule(options: { handler: ({ match, chain, range }) => { if (match.input) { chain().deleteRange(range).run() - Promise.resolve(options.getAttributes(match)).then((attributes) => { - if (attributes) { - options.editor.commands.insertContent({ - type: options.type.name, - attrs: attributes, - }) - } - }) + const attributes = options.getAttributes(match) + if (attributes) { + options.editor.commands.insertContent({ + type: options.type.name, + attrs: attributes, + }) + } } }, }) diff --git a/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx b/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx index a1a8801ce358a..d5c98de3f94ac 100644 --- a/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx +++ b/frontend/src/scenes/notebooks/Notebook/MentionsExtension.tsx @@ -123,7 +123,7 @@ export const Mentions = forwardRef(function SlashCom status="primary-alt" icon={} active={index === selectedIndex} - onClick={async () => await execute(member)} + onClick={() => void execute(member)} > {`${member.user.first_name} <${member.user.email}>`} diff --git a/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx b/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx index c40031ad20dfc..8eda1fc7d11da 100644 --- a/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx +++ b/frontend/src/scenes/notebooks/Notebook/NotebookShare.tsx @@ -31,7 +31,7 @@ export function NotebookShare({ shortId }: NotebookShareProps): JSX.Element { fullWidth center sideIcon={} - onClick={async () => await copyToClipboard(url, 'notebook link')} + onClick={() => void copyToClipboard(url, 'notebook link')} title={url} > {url} diff --git a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx index 07c1b4610b3d9..d054e49811fa9 100644 --- a/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx +++ b/frontend/src/scenes/notebooks/Notebook/SlashCommands.tsx @@ -465,7 +465,7 @@ export const SlashCommands = forwardRef(fu status="primary-alt" size="small" active={selectedIndex === -1 && selectedHorizontalIndex === index} - onClick={async () => await execute(item)} + onClick={() => void execute(item)} icon={item.icon} /> ))} @@ -480,7 +480,7 @@ export const SlashCommands = forwardRef(fu status="primary-alt" icon={item.icon} active={index === selectedIndex} - onClick={async () => await execute(item)} + onClick={() => void execute(item)} > {item.title} diff --git a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx index e8115d47c6dcb..350c675325171 100644 --- a/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx +++ b/frontend/src/scenes/notebooks/NotebookSelectButton/NotebookSelectButton.tsx @@ -86,9 +86,9 @@ export function NotebookSelectList(props: NotebookSelectProps): JSX.Element { const { setShowPopover, setSearchQuery, loadNotebooksContainingResource, loadAllNotebooks } = useActions(logic) const { createNotebook } = useActions(notebooksModel) - const openAndAddToNotebook = async (notebookShortId: string, exists: boolean): Promise => { + const openAndAddToNotebook = (notebookShortId: string, exists: boolean): void => { const position = props.resource ? 'end' : 'start' - await openNotebook(notebookShortId, NotebookTarget.Popover, position, (theNotebookLogic) => { + void openNotebook(notebookShortId, NotebookTarget.Popover, position, (theNotebookLogic) => { if (!exists && props.resource) { theNotebookLogic.actions.insertAfterLastNode([props.resource]) } @@ -169,9 +169,9 @@ export function NotebookSelectList(props: NotebookSelectProps): JSX.Element { emptyState={ searchQuery.length ? 'No matching notebooks' : 'Not already in any notebooks' } - onClick={async (notebookShortId) => { + onClick={(notebookShortId) => { setShowPopover(false) - await openAndAddToNotebook(notebookShortId, true) + openAndAddToNotebook(notebookShortId, true) }} /> @@ -181,9 +181,9 @@ export function NotebookSelectList(props: NotebookSelectProps): JSX.Element { { + onClick={(notebookShortId) => { setShowPopover(false) - await openAndAddToNotebook(notebookShortId, false) + openAndAddToNotebook(notebookShortId, false) }} /> diff --git a/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx b/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx index 5c2522459248d..bb10007b72fd9 100644 --- a/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx +++ b/frontend/src/scenes/notebooks/NotebooksTable/NotebooksTable.tsx @@ -117,7 +117,7 @@ export function NotebooksTable(): JSX.Element { Created by: ({ value: x.user.uuid, label: x.user.first_name, diff --git a/frontend/src/scenes/notebooks/NotebooksTable/notebooksTableLogic.ts b/frontend/src/scenes/notebooks/NotebooksTable/notebooksTableLogic.ts index c42997eef9adf..b234af21f1270 100644 --- a/frontend/src/scenes/notebooks/NotebooksTable/notebooksTableLogic.ts +++ b/frontend/src/scenes/notebooks/NotebooksTable/notebooksTableLogic.ts @@ -33,7 +33,7 @@ export const notebooksTableLogic = kea([ }), reducers({ filters: [ - DEFAULT_FILTERS as NotebooksListFilters, + DEFAULT_FILTERS, { setFilters: (state, { filters }) => objectClean({ diff --git a/frontend/src/scenes/onboarding/Onboarding.tsx b/frontend/src/scenes/onboarding/Onboarding.tsx index e192c59e456d4..f7e01007b81cf 100644 --- a/frontend/src/scenes/onboarding/Onboarding.tsx +++ b/frontend/src/scenes/onboarding/Onboarding.tsx @@ -1,9 +1,6 @@ import { useActions, useValues } from 'kea' -import { FEATURE_FLAGS } from 'lib/constants' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { useEffect, useState } from 'react' import { SceneExport } from 'scenes/sceneTypes' -import { urls } from 'scenes/urls' import { ProductKey } from '~/types' @@ -122,15 +119,8 @@ const SurveysOnboarding = (): JSX.Element => { } export function Onboarding(): JSX.Element | null { - const { featureFlags } = useValues(featureFlagLogic) const { product } = useValues(onboardingLogic) - useEffect(() => { - if (featureFlags[FEATURE_FLAGS.PRODUCT_SPECIFIC_ONBOARDING] !== 'test') { - location.href = urls.ingestion() - } - }, []) - if (!product) { return <> } diff --git a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx index 14db7680557f8..ac13ae7f4dcc7 100644 --- a/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingBillingStep.tsx @@ -1,4 +1,4 @@ -import { LemonButton } from '@posthog/lemon-ui' +import { LemonBanner, LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { StarHog } from 'lib/components/hedgehogs' import { IconCheckCircleOutline } from 'lib/lemon-ui/icons' @@ -27,6 +27,7 @@ export const OnboardingBillingStep = ({ const { currentAndUpgradePlans } = useValues(billingProductLogic({ product })) const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) const plan = currentAndUpgradePlans?.upgradePlan + const currentPlan = currentAndUpgradePlans?.currentPlan return ( {product.subscribed ? (
-
+
@@ -66,6 +67,15 @@ export const OnboardingBillingStep = ({
+ {currentPlan.initial_billing_limit && ( +
+ + To protect your costs and ours, this product has an initial billing limit of $ + {currentPlan.initial_billing_limit}. You can change or remove this limit on the + Billing page. + +
+ )}
) : ( <> diff --git a/frontend/src/scenes/onboarding/onboardingLogic.tsx b/frontend/src/scenes/onboarding/onboardingLogic.tsx index 9bd5b49eff7ae..f79b06d1a9011 100644 --- a/frontend/src/scenes/onboarding/onboardingLogic.tsx +++ b/frontend/src/scenes/onboarding/onboardingLogic.tsx @@ -74,7 +74,7 @@ export const onboardingLogic = kea([ allOnboardingSteps: [ [] as AllOnboardingSteps, { - setAllOnboardingSteps: (_, { allOnboardingSteps }) => allOnboardingSteps as AllOnboardingSteps, + setAllOnboardingSteps: (_, { allOnboardingSteps }) => allOnboardingSteps, }, ], stepKey: [ @@ -84,7 +84,7 @@ export const onboardingLogic = kea([ }, ], onCompleteOnboardingRedirectUrl: [ - urls.default() as string, + urls.default(), { setProductKey: (_, { productKey }) => { return productKey ? getProductUri(productKey as ProductKey) : urls.default() @@ -153,7 +153,9 @@ export const onboardingLogic = kea([ }), listeners(({ actions, values }) => ({ loadBillingSuccess: () => { - actions.setProduct(values.billing?.products.find((p) => p.type === values.productKey) || null) + if (window.location.pathname.startsWith('/onboarding')) { + actions.setProduct(values.billing?.products.find((p) => p.type === values.productKey) || null) + } }, setProduct: ({ product }) => { if (!product) { @@ -205,7 +207,7 @@ export const onboardingLogic = kea([ } }, resetStepKey: () => { - actions.setStepKey(values.allOnboardingSteps[0].props.stepKey) + values.allOnboardingSteps[0] && actions.setStepKey(values.allOnboardingSteps[0]?.props.stepKey) }, })), actionToUrl(({ values }) => ({ diff --git a/frontend/src/scenes/paths/PathNodeCardButton.tsx b/frontend/src/scenes/paths/PathNodeCardButton.tsx index 3cc644f5555a8..e28ae334cb551 100644 --- a/frontend/src/scenes/paths/PathNodeCardButton.tsx +++ b/frontend/src/scenes/paths/PathNodeCardButton.tsx @@ -1,4 +1,5 @@ import { LemonButton, LemonButtonWithDropdown } from '@posthog/lemon-ui' +import { captureException } from '@sentry/react' import { useValues } from 'kea' import { IconEllipsis } from 'lib/lemon-ui/icons' import { copyToClipboard } from 'lib/utils' @@ -39,8 +40,8 @@ export function PathNodeCardButton({ const viewFunnel = (): void => { viewPathToFunnel(node) } - const copyName = async (): Promise => { - await copyToClipboard(pageUrl(node)) + const copyName = (): void => { + void copyToClipboard(pageUrl(node)).then(captureException) } const openModal = (): void => openPersonsModal({ path_end_key: name }) diff --git a/frontend/src/scenes/pipeline/Pipeline.stories.tsx b/frontend/src/scenes/pipeline/Pipeline.stories.tsx index 842214bb3cb14..c59cacefe547e 100644 --- a/frontend/src/scenes/pipeline/Pipeline.stories.tsx +++ b/frontend/src/scenes/pipeline/Pipeline.stories.tsx @@ -5,7 +5,7 @@ import { App } from 'scenes/App' import { urls } from 'scenes/urls' import { mswDecorator, useStorybookMocks } from '~/mocks/browser' -import { PipelineTabs } from '~/types' +import { PipelineAppTabs, PipelineTabs } from '~/types' import { pipelineLogic } from './pipelineLogic' @@ -60,3 +60,29 @@ export function PipelineTransformationsPage(): JSX.Element { }, []) return } + +export function PipelineAppConfiguration(): JSX.Element { + useEffect(() => { + router.actions.push(urls.pipelineApp(1, PipelineAppTabs.Configuration)) + }, []) + return +} + +export function PipelineAppMetrics(): JSX.Element { + useEffect(() => { + router.actions.push(urls.pipelineApp(1, PipelineAppTabs.Metrics)) + }, []) + return +} + +export function PipelineAppLogs(): JSX.Element { + useStorybookMocks({ + get: { + 'api/projects/:team_id/plugin_configs/1/logs': require('./__mocks__/pluginLogs.json'), + }, + }) + useEffect(() => { + router.actions.push(urls.pipelineApp(1, PipelineAppTabs.Logs)) + }, []) + return +} diff --git a/frontend/src/scenes/pipeline/PipelineApp.tsx b/frontend/src/scenes/pipeline/PipelineApp.tsx new file mode 100644 index 0000000000000..3ca9430e7d8a2 --- /dev/null +++ b/frontend/src/scenes/pipeline/PipelineApp.tsx @@ -0,0 +1,50 @@ +import { Spinner } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { router } from 'kea-router' +import { PageHeader } from 'lib/components/PageHeader' +import { LemonTabs } from 'lib/lemon-ui/LemonTabs/LemonTabs' +import { capitalizeFirstLetter } from 'lib/utils' +import { PluginLogs } from 'scenes/plugins/plugin/PluginLogs' +import { SceneExport } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { PipelineAppTabs } from '~/types' + +import { pipelineAppLogic } from './pipelineAppLogic' + +export const scene: SceneExport = { + component: PipelineApp, + logic: pipelineAppLogic, + paramsToProps: ({ params: { id } }: { params: { id?: string } }) => ({ id: id ? parseInt(id) : 'new' }), +} + +export function PipelineApp({ id }: { id?: string } = {}): JSX.Element { + const { currentTab } = useValues(pipelineAppLogic) + + const confId = id ? parseInt(id) : undefined + + if (!confId) { + return + } + + const tab_to_content: Record = { + [PipelineAppTabs.Configuration]:
Configuration editing
, + [PipelineAppTabs.Metrics]:
Metrics page
, + [PipelineAppTabs.Logs]: , + } + + return ( +
+ + router.actions.push(urls.pipelineApp(confId, tab as PipelineAppTabs))} + tabs={Object.values(PipelineAppTabs).map((tab) => ({ + label: capitalizeFirstLetter(tab), + key: tab, + content: tab_to_content[tab], + }))} + /> +
+ ) +} diff --git a/frontend/src/scenes/pipeline/Transformations.tsx b/frontend/src/scenes/pipeline/Transformations.tsx index 2849674584676..65e5574acabff 100644 --- a/frontend/src/scenes/pipeline/Transformations.tsx +++ b/frontend/src/scenes/pipeline/Transformations.tsx @@ -23,7 +23,7 @@ import { deleteWithUndo, humanFriendlyDetailedTime } from 'lib/utils' import { PluginImage } from 'scenes/plugins/plugin/PluginImage' import { urls } from 'scenes/urls' -import { PipelineTabs, PluginConfigTypeNew, PluginType, ProductKey } from '~/types' +import { PipelineAppTabs, PipelineTabs, PluginConfigTypeNew, PluginType, ProductKey } from '~/types' import { NewButton } from './NewButton' import { pipelineTransformationsLogic } from './transformationsLogic' @@ -102,7 +102,12 @@ export function Transformations(): JSX.Element { return ( <> - + {pluginConfig.name} @@ -155,7 +160,9 @@ export function Transformations(): JSX.Element { } > - + Error @@ -209,7 +216,10 @@ export function Transformations(): JSX.Element { )} @@ -217,7 +227,7 @@ export function Transformations(): JSX.Element { @@ -225,7 +235,7 @@ export function Transformations(): JSX.Element { @@ -246,7 +256,7 @@ export function Transformations(): JSX.Element { { - deleteWithUndo({ + void deleteWithUndo({ endpoint: `plugin_config`, object: { id: pluginConfig.id, diff --git a/frontend/src/scenes/pipeline/__mocks__/pluginLogs.json b/frontend/src/scenes/pipeline/__mocks__/pluginLogs.json new file mode 100644 index 0000000000000..09fa96824bf74 --- /dev/null +++ b/frontend/src/scenes/pipeline/__mocks__/pluginLogs.json @@ -0,0 +1,29 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "id": "018bb51f-0f9f-0000-34ae-d3aa1d9a5770", + "team_id": 1, + "plugin_id": 1, + "plugin_config_id": 11, + "timestamp": "2023-11-09T17:26:33.626000Z", + "source": "PLUGIN", + "type": "ERROR", + "message": "Error: Received an unexpected error from the endpoint API. Response 400: {\"meta\":{\"errors\":[\"value for attribute '$current_url' cannot be longer than 1000 bytes\"]}}\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)", + "instance_id": "12345678-1234-1234-1234-123456789012" + }, + { + "id": "018bb51e-262a-0000-eb34-39afd4691d56", + "team_id": 1, + "plugin_id": 1, + "plugin_config_id": 11, + "timestamp": "2023-11-09T17:25:33.790000Z", + "source": "PLUGIN", + "type": "INFO", + "message": "Successfully sent event to endpoint", + "instance_id": "12345678-1234-1234-1234-123456789012" + } + ] +} diff --git a/frontend/src/scenes/pipeline/pipelineAppLogic.tsx b/frontend/src/scenes/pipeline/pipelineAppLogic.tsx new file mode 100644 index 0000000000000..6d6b691f2c275 --- /dev/null +++ b/frontend/src/scenes/pipeline/pipelineAppLogic.tsx @@ -0,0 +1,54 @@ +import { actions, kea, key, path, props, reducers, selectors } from 'kea' +import { actionToUrl, urlToAction } from 'kea-router' +import { urls } from 'scenes/urls' + +import { Breadcrumb, PipelineAppTabs } from '~/types' + +import type { pipelineAppLogicType } from './pipelineAppLogicType' + +export interface PipelineAppLogicProps { + id: number +} + +export const pipelineAppLogic = kea([ + props({} as PipelineAppLogicProps), + key(({ id }) => id), + path((id) => ['scenes', 'pipeline', 'pipelineAppLogic', id]), + actions({ + setCurrentTab: (tab: PipelineAppTabs = PipelineAppTabs.Configuration) => ({ tab }), + }), + reducers({ + currentTab: [ + PipelineAppTabs.Configuration as PipelineAppTabs, + { + setCurrentTab: (_, { tab }) => tab, + }, + ], + }), + selectors({ + breadcrumbs: [ + () => [], + (): Breadcrumb[] => [ + { + name: 'Pipeline', + path: urls.pipeline(), + }, + { + name: 'App name', + }, + ], + ], + }), + actionToUrl(({ values, props }) => { + return { + setCurrentTab: () => [urls.pipelineApp(props.id, values.currentTab)], + } + }), + urlToAction(({ actions, values }) => ({ + '/pipeline/:id/:tab': ({ tab }) => { + if (tab !== values.currentTab) { + actions.setCurrentTab(tab as PipelineAppTabs) + } + }, + })), +]) diff --git a/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx b/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx index c12ed86386265..cb09686bd9b19 100644 --- a/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx +++ b/frontend/src/scenes/plugins/edit/interface-jobs/PluginJobConfiguration.tsx @@ -1,6 +1,5 @@ import { IconCheck } from '@posthog/icons' -import { Tooltip } from '@posthog/lemon-ui' -import { InputNumber, Radio } from 'antd' +import { LemonSegmentedButton, Tooltip } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { ChildFunctionProps, Form } from 'kea-forms' import { CodeEditor } from 'lib/components/CodeEditors' @@ -108,7 +107,7 @@ function FieldInput({ case 'string': return case 'number': - return + return case 'json': return ( onChange(e.target.value)} - > - - True - - - False - - + options={[ + { + value: true, + label: 'True', + icon: , + }, + { + value: false, + label: 'False', + icon: , + }, + ]} + /> ) case 'date': return ( diff --git a/frontend/src/scenes/plugins/plugin/PluginLogs.tsx b/frontend/src/scenes/plugins/plugin/PluginLogs.tsx index 40b404e79fc17..f2851c2c64fcd 100644 --- a/frontend/src/scenes/plugins/plugin/PluginLogs.tsx +++ b/frontend/src/scenes/plugins/plugin/PluginLogs.tsx @@ -52,6 +52,7 @@ const columns: LemonTableColumns> = [ title: 'Message', key: 'message', dataIndex: 'message', + render: (message: string) => {message}, }, ] diff --git a/frontend/src/scenes/plugins/pluginsLogic.ts b/frontend/src/scenes/plugins/pluginsLogic.ts index 49bbad9d0d610..c3c743d0144ee 100644 --- a/frontend/src/scenes/plugins/pluginsLogic.ts +++ b/frontend/src/scenes/plugins/pluginsLogic.ts @@ -624,13 +624,13 @@ export const pluginsLogic = kea([ (s) => [s.repository, s.plugins], (repository, plugins) => { const allPossiblePlugins: PluginSelectionType[] = [] - for (const plugin of Object.values(plugins) as PluginType[]) { + for (const plugin of Object.values(plugins)) { allPossiblePlugins.push({ name: plugin.name, url: plugin.url }) } const installedUrls = new Set(Object.values(plugins).map((plugin) => plugin.url)) - for (const plugin of Object.values(repository) as PluginRepositoryEntry[]) { + for (const plugin of Object.values(repository)) { if (!installedUrls.has(plugin.url)) { allPossiblePlugins.push({ name: plugin.name, url: plugin.url }) } diff --git a/frontend/src/scenes/plugins/source/PluginSource.tsx b/frontend/src/scenes/plugins/source/PluginSource.tsx index cb4da64c92d3d..ad47b039462bf 100644 --- a/frontend/src/scenes/plugins/source/PluginSource.tsx +++ b/frontend/src/scenes/plugins/source/PluginSource.tsx @@ -58,7 +58,7 @@ export function PluginSource({ if (!monaco) { return } - import('./types/packages.json').then((files) => { + void import('./types/packages.json').then((files) => { for (const [fileName, fileContents] of Object.entries(files).filter( ([fileName]) => fileName !== 'default' )) { diff --git a/frontend/src/scenes/plugins/tabs/apps/components.tsx b/frontend/src/scenes/plugins/tabs/apps/components.tsx index 306ab76b7d201..5cd0be154fd71 100644 --- a/frontend/src/scenes/plugins/tabs/apps/components.tsx +++ b/frontend/src/scenes/plugins/tabs/apps/components.tsx @@ -24,7 +24,7 @@ export function RepositoryTag({ plugin }: { plugin: PluginType | PluginRepositor if (plugin.plugin_type === 'local' && plugin.url) { return ( - await copyToClipboard(plugin.url?.substring(5) || '')}> + void copyToClipboard(plugin.url?.substring(5) || '')}> Installed Locally ) diff --git a/frontend/src/scenes/products/Products.tsx b/frontend/src/scenes/products/Products.tsx index 7a10b30105c7f..14a30c0cad323 100644 --- a/frontend/src/scenes/products/Products.tsx +++ b/frontend/src/scenes/products/Products.tsx @@ -2,11 +2,8 @@ import * as Icons from '@posthog/icons' import { LemonButton } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { router } from 'kea-router' -import { FEATURE_FLAGS } from 'lib/constants' import { LemonCard } from 'lib/lemon-ui/LemonCard/LemonCard' import { Spinner } from 'lib/lemon-ui/Spinner' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' -import { useEffect } from 'react' import { billingLogic } from 'scenes/billing/billingLogic' import { getProductUri } from 'scenes/onboarding/onboardingLogic' import { SceneExport } from 'scenes/sceneTypes' @@ -133,18 +130,11 @@ export function ProductCard({ } export function Products(): JSX.Element { - const { featureFlags } = useValues(featureFlagLogic) const { billing } = useValues(billingLogic) const { currentTeam } = useValues(teamLogic) const isFirstProduct = Object.keys(currentTeam?.has_completed_onboarding_for || {}).length === 0 const products = billing?.products || [] - useEffect(() => { - if (featureFlags[FEATURE_FLAGS.PRODUCT_SPECIFIC_ONBOARDING] !== 'test') { - location.href = urls.ingestion() - } - }, []) - return (
diff --git a/frontend/src/scenes/products/productsLogic.tsx b/frontend/src/scenes/products/productsLogic.tsx index cbdab7e5f4fb7..313e26ea70a1c 100644 --- a/frontend/src/scenes/products/productsLogic.tsx +++ b/frontend/src/scenes/products/productsLogic.tsx @@ -1,5 +1,6 @@ -import { actions, kea, listeners, path } from 'kea' +import { actions, connect, kea, listeners, path } from 'kea' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { onboardingLogic } from 'scenes/onboarding/onboardingLogic' import { teamLogic } from 'scenes/teamLogic' import { ProductKey } from '~/types' @@ -8,10 +9,13 @@ import type { productsLogicType } from './productsLogicType' export const productsLogic = kea([ path(() => ['scenes', 'products', 'productsLogic']), + connect({ + actions: [teamLogic, ['updateCurrentTeam'], onboardingLogic, ['setProduct']], + }), actions(() => ({ onSelectProduct: (product: ProductKey) => ({ product }), })), - listeners(() => ({ + listeners(({ actions }) => ({ onSelectProduct: ({ product }) => { eventUsageLogic.actions.reportOnboardingProductSelected(product) @@ -19,7 +23,7 @@ export const productsLogic = kea([ case ProductKey.PRODUCT_ANALYTICS: return case ProductKey.SESSION_REPLAY: - teamLogic.actions.updateCurrentTeam({ + actions.updateCurrentTeam({ session_recording_opt_in: true, capture_console_log_opt_in: true, capture_performance_opt_in: true, diff --git a/frontend/src/scenes/retention/RetentionModal.tsx b/frontend/src/scenes/retention/RetentionModal.tsx index 8356a7f72ac9f..e3f364621c592 100644 --- a/frontend/src/scenes/retention/RetentionModal.tsx +++ b/frontend/src/scenes/retention/RetentionModal.tsx @@ -46,7 +46,7 @@ export function RetentionModal(): JSX.Element | null { - triggerExport({ + void triggerExport({ export_format: ExporterFormat.CSV, export_context: { path: row?.people_url, diff --git a/frontend/src/scenes/retention/retentionPeopleLogic.ts b/frontend/src/scenes/retention/retentionPeopleLogic.ts index 2e6bbdc72f733..2cc3b73e9a7f1 100644 --- a/frontend/src/scenes/retention/retentionPeopleLogic.ts +++ b/frontend/src/scenes/retention/retentionPeopleLogic.ts @@ -31,7 +31,7 @@ export const retentionPeopleLogic = kea([ __default: {} as RetentionTablePeoplePayload, loadPeople: async (rowIndex: number) => { const urlParams = toParams({ ...values.apiFilters, selected_interval: rowIndex }) - return (await api.get(`api/person/retention/?${urlParams}`)) as RetentionTablePeoplePayload + return await api.get(`api/person/retention/?${urlParams}`) }, }, })), diff --git a/frontend/src/scenes/saved-insights/SavedInsights.tsx b/frontend/src/scenes/saved-insights/SavedInsights.tsx index 0d8bd5d0f5302..94631bbd752a0 100644 --- a/frontend/src/scenes/saved-insights/SavedInsights.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsights.tsx @@ -355,8 +355,8 @@ function SavedInsightsGrid(): JSX.Element { insight={{ ...insight }} rename={() => renameInsight(insight)} duplicate={() => duplicateInsight(insight)} - deleteWithUndo={() => - deleteWithUndo({ + deleteWithUndo={async () => + await deleteWithUndo({ object: insight, endpoint: `projects/${currentTeamId}/insights`, callback: loadInsights, @@ -503,7 +503,7 @@ export function SavedInsights(): JSX.Element { - deleteWithUndo({ + void deleteWithUndo({ object: insight, endpoint: `projects/${currentTeamId}/insights`, callback: loadInsights, diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts index 51e67ede119ae..37957afac97c9 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.test.ts @@ -195,7 +195,7 @@ describe('savedInsightsLogic', () => { const sourceInsight = createInsight(123, 'hello') sourceInsight.name = '' sourceInsight.derived_name = 'should be copied' - await logic.actions.duplicateInsight(sourceInsight) + await logic.asyncActions.duplicateInsight(sourceInsight) expect(api.create).toHaveBeenCalledWith( `api/projects/${MOCK_TEAM_ID}/insights`, expect.objectContaining({ name: '' }) @@ -206,7 +206,7 @@ describe('savedInsightsLogic', () => { const sourceInsight = createInsight(123, 'hello') sourceInsight.name = 'should be copied' sourceInsight.derived_name = '' - await logic.actions.duplicateInsight(sourceInsight) + await logic.asyncActions.duplicateInsight(sourceInsight) expect(api.create).toHaveBeenCalledWith( `api/projects/${MOCK_TEAM_ID}/insights`, expect.objectContaining({ name: 'should be copied (copy)' }) diff --git a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts index 0a96ea35672df..cb1f40ff676d0 100644 --- a/frontend/src/scenes/saved-insights/savedInsightsLogic.ts +++ b/frontend/src/scenes/saved-insights/savedInsightsLogic.ts @@ -40,7 +40,7 @@ export interface SavedInsightFilters { search: string insightType: string createdBy: number | 'All users' - dateFrom: string | dayjs.Dayjs | undefined | 'all' | null + dateFrom: string | dayjs.Dayjs | undefined | null dateTo: string | dayjs.Dayjs | undefined | null page: number dashboardId: number | undefined | null diff --git a/frontend/src/scenes/sceneLogic.ts b/frontend/src/scenes/sceneLogic.ts index 7b9e9fe13de0c..a08bfab9ef878 100644 --- a/frontend/src/scenes/sceneLogic.ts +++ b/frontend/src/scenes/sceneLogic.ts @@ -1,7 +1,5 @@ import { actions, BuiltLogic, connect, kea, listeners, path, props, reducers, selectors } from 'kea' import { router, urlToAction } from 'kea-router' -import { FEATURE_FLAGS } from 'lib/constants' -import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import posthog from 'posthog-js' import { emptySceneParams, preloadedScenes, redirects, routes, sceneConfigurations } from 'scenes/scenes' @@ -258,21 +256,12 @@ export const sceneLogic = kea([ !location.pathname.startsWith('/settings') ) { if ( - featureFlagLogic.values.featureFlags[FEATURE_FLAGS.PRODUCT_SPECIFIC_ONBOARDING] === - 'test' && + !teamLogic.values.currentTeam.completed_snippet_onboarding && !Object.keys(teamLogic.values.currentTeam.has_completed_onboarding_for || {}).length ) { - console.warn('No onboarding completed, redirecting to products') + console.warn('No onboarding completed, redirecting to /products') router.actions.replace(urls.products()) return - } else if ( - featureFlagLogic.values.featureFlags[FEATURE_FLAGS.PRODUCT_SPECIFIC_ONBOARDING] !== - 'test' && - !teamLogic.values.currentTeam.completed_snippet_onboarding - ) { - console.warn('Ingestion tutorial not completed, redirecting to it') - router.actions.replace(urls.ingestion()) - return } } } diff --git a/frontend/src/scenes/sceneTypes.ts b/frontend/src/scenes/sceneTypes.ts index 5d5ed7a89c3ff..3f41023e13f63 100644 --- a/frontend/src/scenes/sceneTypes.ts +++ b/frontend/src/scenes/sceneTypes.ts @@ -22,6 +22,7 @@ export enum Scene { PersonsManagement = 'PersonsManagement', Person = 'Person', Pipeline = 'Pipeline', + PipelineApp = 'PipelineApp', Group = 'Group', Action = 'Action', Experiments = 'Experiments', @@ -64,7 +65,6 @@ export enum Scene { PasswordReset = 'PasswordReset', PasswordResetComplete = 'PasswordResetComplete', PreflightCheck = 'PreflightCheck', - Ingestion = 'IngestionWizard', OrganizationCreationConfirm = 'OrganizationCreationConfirm', Unsubscribe = 'Unsubscribe', DebugQuery = 'DebugQuery', diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index 5a6beafb1623e..f9957df401826 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -9,7 +9,7 @@ import { Error404 as Error404Component } from '~/layout/Error404' import { ErrorNetwork as ErrorNetworkComponent } from '~/layout/ErrorNetwork' import { ErrorProjectUnavailable as ErrorProjectUnavailableComponent } from '~/layout/ErrorProjectUnavailable' import { EventsQuery } from '~/queries/schema' -import { InsightShortId, PipelineTabs, PropertyFilterType, ReplayTabs } from '~/types' +import { InsightShortId, PipelineAppTabs, PipelineTabs, PropertyFilterType, ReplayTabs } from '~/types' export const emptySceneParams = { params: {}, searchParams: {}, hashParams: {} } @@ -113,6 +113,10 @@ export const sceneConfigurations: Partial> = { projectBased: true, name: 'Pipeline', }, + [Scene.PipelineApp]: { + projectBased: true, + name: 'Pipeline app', + }, [Scene.Experiments]: { projectBased: true, name: 'Experiments', @@ -193,10 +197,6 @@ export const sceneConfigurations: Partial> = { [Scene.IntegrationsRedirect]: { name: 'Integrations Redirect', }, - [Scene.Ingestion]: { - projectBased: true, - layout: 'plain', - }, [Scene.Products]: { projectBased: true, layout: 'plain', @@ -406,10 +406,14 @@ export const routes: Record = { [urls.persons()]: Scene.PersonsManagement, [urls.pipeline()]: Scene.Pipeline, // One entry for every available tab - ...Object.values(PipelineTabs).reduce((acc, tab) => { - acc[urls.pipeline(tab)] = Scene.Pipeline - return acc - }, {} as Record), + ...(Object.fromEntries(Object.values(PipelineTabs).map((tab) => [urls.pipeline(tab), Scene.Pipeline])) as Record< + string, + Scene + >), + // One entry for each available tab (key by app config id) + ...(Object.fromEntries( + Object.values(PipelineAppTabs).map((tab) => [urls.pipelineApp(':id', tab), Scene.PipelineApp]) + ) as Record), [urls.groups(':groupTypeIndex')]: Scene.PersonsManagement, [urls.group(':groupTypeIndex', ':groupKey', false)]: Scene.Group, [urls.group(':groupTypeIndex', ':groupKey', false, ':groupTab')]: Scene.Group, @@ -465,8 +469,6 @@ export const routes: Record = { [urls.inviteSignup(':id')]: Scene.InviteSignup, [urls.passwordReset()]: Scene.PasswordReset, [urls.passwordResetComplete(':uuid', ':token')]: Scene.PasswordResetComplete, - [urls.ingestion()]: Scene.Ingestion, - [urls.ingestion() + '/*']: Scene.Ingestion, [urls.products()]: Scene.Products, [urls.onboarding(':productKey')]: Scene.Onboarding, [urls.verifyEmail()]: Scene.VerifyEmail, diff --git a/frontend/src/scenes/session-recordings/filters/DurationFilter.test.ts b/frontend/src/scenes/session-recordings/filters/DurationFilter.test.ts index fe196b529e062..bfc4fb0efff0a 100644 --- a/frontend/src/scenes/session-recordings/filters/DurationFilter.test.ts +++ b/frontend/src/scenes/session-recordings/filters/DurationFilter.test.ts @@ -34,7 +34,7 @@ describe('DurationFilter', () => { [PropertyOperator.GreaterThan, 3601, 'inactive_seconds', '> 3601 inactive seconds'], [PropertyOperator.GreaterThan, 3660, 'inactive_seconds', '> 61 inactive minutes'], [PropertyOperator.LessThan, 0, 'active_seconds', '< 0 active seconds'], - ])('converts the value correctly for total duration', async (operator, value, durationType, expectation) => { + ])('converts the value correctly for total duration', (operator, value, durationType, expectation) => { const filter: RecordingDurationFilter = { type: PropertyFilterType.Recording, key: 'duration', diff --git a/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts b/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts index c1564a504b0a7..838f51c78e6f5 100644 --- a/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/modal/sessionPlayerModalLogic.test.ts @@ -29,7 +29,7 @@ describe('sessionPlayerModalLogic', () => { it('starts as null', () => { expectLogic(logic).toMatchValues({ activeSessionRecording: null }) }) - it('is set by openSessionPlayer and cleared by closeSessionPlayer', async () => { + it('is set by openSessionPlayer and cleared by closeSessionPlayer', () => { expectLogic(logic, () => logic.actions.openSessionPlayer({ id: 'abc' })) .toDispatchActions(['loadSessionRecordingsSuccess']) .toMatchValues({ diff --git a/frontend/src/scenes/session-recordings/player/playerSettingsLogic.test.ts b/frontend/src/scenes/session-recordings/player/playerSettingsLogic.test.ts index 61173382a42fd..da212eb7a77bd 100644 --- a/frontend/src/scenes/session-recordings/player/playerSettingsLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/playerSettingsLogic.test.ts @@ -59,7 +59,7 @@ describe('playerSettingsLogic', () => { afterEach(() => { localStorage.clear() }) - it('should start with the first entry selected', async () => { + it('should start with the first entry selected', () => { expect(logic.values.selectedMiniFilters).toEqual([ 'all-automatic', 'console-all', @@ -68,7 +68,7 @@ describe('playerSettingsLogic', () => { ]) }) - it('should remove other selected filters if alone', async () => { + it('should remove other selected filters if alone', () => { logic.actions.setMiniFilter('all-errors', true) expect(logic.values.selectedMiniFilters.sort()).toEqual([ @@ -79,7 +79,7 @@ describe('playerSettingsLogic', () => { ]) }) - it('should allow multiple filters if not alone', async () => { + it('should allow multiple filters if not alone', () => { logic.actions.setMiniFilter('console-warn', true) logic.actions.setMiniFilter('console-info', true) @@ -92,7 +92,7 @@ describe('playerSettingsLogic', () => { ]) }) - it('should reset to first in tab if empty', async () => { + it('should reset to first in tab if empty', () => { expect(logic.values.selectedMiniFilters.sort()).toEqual([ 'all-automatic', 'console-all', diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts index 4cf85f31551a7..14ad52c0a62c4 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.test.ts @@ -24,7 +24,7 @@ const sortedRecordingSnapshotsJson = sortedRecordingSnapshots() describe('sessionRecordingDataLogic', () => { let logic: ReturnType - beforeEach(async () => { + beforeEach(() => { useAvailableFeatures([AvailableFeature.RECORDINGS_PERFORMANCE]) useMocks({ get: { @@ -67,7 +67,7 @@ describe('sessionRecordingDataLogic', () => { it('mounts other logics', async () => { await expectLogic(logic).toMount([eventUsageLogic, teamLogic, userLogic]) }) - it('has default values', async () => { + it('has default values', () => { expect(logic.values).toMatchObject({ bufferedToTime: null, durationMs: 0, diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts index dbe1837f4a481..623a902ae428e 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingDataLogic.ts @@ -267,7 +267,7 @@ export const sessionRecordingDataLogic = kea([ reportViewed: async (_, breakpoint) => { const durations = generateRecordingReportDurations(cache, values) - await breakpoint() + breakpoint() // Triggered on first paint eventUsageLogic.actions.reportRecording( values.sessionPlayerData, diff --git a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts index 8bf96dc87894d..5d865c9dda4c8 100644 --- a/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts +++ b/frontend/src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts @@ -607,7 +607,7 @@ export const sessionRecordingPlayerLogic = kea( // If replayer isn't initialized, it will be initialized with the already loaded snapshots if (values.player?.replayer) { for (const event of eventsToAdd) { - await values.player?.replayer?.addEvent(event) + values.player?.replayer?.addEvent(event) } } @@ -617,7 +617,7 @@ export const sessionRecordingPlayerLogic = kea( actions.checkBufferingCompleted() breakpoint() }, - loadRecordingMetaSuccess: async () => { + loadRecordingMetaSuccess: () => { // As the connected data logic may be preloaded we call a shared function here and on mount actions.updateFromMetadata() if (props.autoPlay) { @@ -626,7 +626,7 @@ export const sessionRecordingPlayerLogic = kea( } }, - loadRecordingSnapshotsSuccess: async () => { + loadRecordingSnapshotsSuccess: () => { // As the connected data logic may be preloaded we call a shared function here and on mount actions.updateFromMetadata() }, @@ -692,7 +692,7 @@ export const sessionRecordingPlayerLogic = kea( actions.reportRecordingPlayerSpeedChanged(speed) actions.syncPlayerSpeed() }, - seekToTimestamp: async ({ timestamp, forcePlay }, breakpoint) => { + seekToTimestamp: ({ timestamp, forcePlay }, breakpoint) => { actions.stopAnimation() actions.setCurrentTimestamp(timestamp) @@ -961,7 +961,7 @@ export const sessionRecordingPlayerLogic = kea( console.warn('Failed to enable native full-screen mode:', e) } } else if (document.fullscreenElement === props.playerRef?.current) { - document.exitFullscreen() + await document.exitFullscreen() } }, })), diff --git a/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx b/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx index 8c17bcd8f97be..affc039108683 100644 --- a/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx +++ b/frontend/src/scenes/session-recordings/player/share/PlayerShare.tsx @@ -1,4 +1,5 @@ import { LemonButton, LemonCheckbox, LemonDivider, LemonInput } from '@posthog/lemon-ui' +import { captureException } from '@sentry/react' import clsx from 'clsx' import { useActions, useValues } from 'kea' import { Form } from 'kea-forms' @@ -28,7 +29,7 @@ export function PlayerShareRecording(props: PlayerShareLogicProps): JSX.Element fullWidth center sideIcon={} - onClick={async () => await copyToClipboard(url, 'recording link')} + onClick={() => void copyToClipboard(url, 'recording link').then(captureException)} title={url} > {url} diff --git a/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts b/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts index 3131d5c3cc370..e08cedf8f036f 100644 --- a/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts +++ b/frontend/src/scenes/session-recordings/player/utils/segmenter.test.ts @@ -8,7 +8,7 @@ import { convertSnapshotsResponse } from '../sessionRecordingDataLogic' import { createSegments } from './segmenter' describe('segmenter', () => { - it('matches snapshots', async () => { + it('matches snapshots', () => { const snapshots = convertSnapshotsResponse(sortedRecordingSnapshots().snapshot_data_by_window_id) const segments = createSegments( snapshots, diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx index 7232b38d2a9e2..37ef17da6ae54 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistTroubleshooting.tsx @@ -1,6 +1,15 @@ import { Link } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' + +import { playerSettingsLogic } from '../player/playerSettingsLogic' +import { sessionRecordingsPlaylistLogic } from './sessionRecordingsPlaylistLogic' export const SessionRecordingsPlaylistTroubleshooting = (): JSX.Element => { + const { hideViewedRecordings } = useValues(playerSettingsLogic) + const { setHideViewedRecordings } = useActions(playerSettingsLogic) + const { otherRecordings } = useValues(sessionRecordingsPlaylistLogic) + const { setShowSettings } = useActions(sessionRecordingsPlaylistLogic) + return ( <>

No matching recordings

@@ -10,6 +19,19 @@ export const SessionRecordingsPlaylistTroubleshooting = (): JSX.Element => {

    + {otherRecordings.length > 0 && hideViewedRecordings && ( +
  • + Viewed recordings hidden.{' '} + { + setShowSettings(true) + setHideViewedRecordings(false) + }} + > + Toggle option + +
  • + )}
  • They are outside the retention period diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts index 1e4dc376d0eb9..8a37208f27945 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsListPropertiesLogic.test.ts @@ -53,7 +53,7 @@ describe('sessionRecordingsListPropertiesLogic', () => { }) it('loads properties', async () => { - await expectLogic(logic, async () => { + await expectLogic(logic, () => { logic.actions.loadPropertiesForSessions(mockSessons) }).toDispatchActions(['loadPropertiesForSessionsSuccess']) @@ -70,7 +70,7 @@ describe('sessionRecordingsListPropertiesLogic', () => { }) it('does not loads cached properties', async () => { - await expectLogic(logic, async () => { + await expectLogic(logic, () => { logic.actions.loadPropertiesForSessions(mockSessons) }).toDispatchActions(['loadPropertiesForSessionsSuccess']) @@ -81,7 +81,7 @@ describe('sessionRecordingsListPropertiesLogic', () => { }, }) - await expectLogic(logic, async () => { + await expectLogic(logic, () => { logic.actions.maybeLoadPropertiesForSessions(mockSessons) }).toNotHaveDispatchedActions(['loadPropertiesForSessionsSuccess']) diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts index 4f419f605c148..36bef7ea8faf8 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.test.ts @@ -167,7 +167,7 @@ describe('sessionRecordingsPlaylistLogic', () => { it('starts as null', () => { expectLogic(logic).toMatchValues({ activeSessionRecording: undefined }) }) - it('is set by setSessionRecordingId', async () => { + it('is set by setSessionRecordingId', () => { expectLogic(logic, () => logic.actions.setSelectedRecordingId('abc')) .toDispatchActions(['loadSessionRecordingsSuccess']) .toMatchValues({ @@ -177,7 +177,7 @@ describe('sessionRecordingsPlaylistLogic', () => { expect(router.values.searchParams).toHaveProperty('sessionRecordingId', 'abc') }) - it('is partial if sessionRecordingId not in list', async () => { + it('is partial if sessionRecordingId not in list', () => { expectLogic(logic, () => logic.actions.setSelectedRecordingId('not-in-list')) .toDispatchActions(['loadSessionRecordingsSuccess']) .toMatchValues({ @@ -200,7 +200,7 @@ describe('sessionRecordingsPlaylistLogic', () => { }) it('mounts and loads the recording when a recording is opened', () => { - expectLogic(logic, async () => await logic.actions.setSelectedRecordingId('abcd')) + expectLogic(logic, async () => logic.asyncActions.setSelectedRecordingId('abcd')) .toMount(sessionRecordingDataLogic({ sessionRecordingId: 'abcd' })) .toDispatchActions(['loadEntireRecording']) }) diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts index 36a451fb46501..130cad3bf210f 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts @@ -351,7 +351,7 @@ export const sessionRecordingsPlaylistLogic = kea ({ ...state, diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.test.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.test.ts index 8d015bdb08915..4eecf62e6634f 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.test.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistSceneLogic.test.ts @@ -65,9 +65,9 @@ describe('sessionRecordingsPlaylistSceneLogic', () => { }, ], } - expectLogic(logic, async () => { - await logic.actions.setFilters(newFilter) - await logic.actions.updatePlaylist({}) + expectLogic(logic, () => { + logic.actions.setFilters(newFilter) + logic.actions.updatePlaylist({}) }) .toDispatchActions(['setFilters']) .toMatchValues({ filters: expect.objectContaining(newFilter), hasChanges: true }) diff --git a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx index d6e85515f07ba..8397d40f303b8 100644 --- a/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx +++ b/frontend/src/scenes/session-recordings/saved-playlists/SavedSessionRecordingPlaylistsEmptyState.tsx @@ -29,7 +29,7 @@ export function SavedSessionRecordingPlaylistsEmptyState(): JSX.Element { AvailableFeature.RECORDINGS_PLAYLISTS, 'recording playlists', "Playlists allow you to save certain session recordings as a group to easily find and watch them again in the future. You've unfortunately run out of playlists on your current subscription plan.", - () => createPlaylist({}, true), + () => void createPlaylist({}, true), undefined, playlists.count ) diff --git a/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.ts b/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.ts index d9fd70ad052d5..b4b4d214c62b6 100644 --- a/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.ts +++ b/frontend/src/scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic.ts @@ -27,7 +27,7 @@ export interface SavedSessionRecordingPlaylistsFilters { order: string search: string createdBy: number | 'All users' - dateFrom: string | dayjs.Dayjs | undefined | 'all' | null + dateFrom: string | dayjs.Dayjs | undefined | null dateTo: string | dayjs.Dayjs | undefined | null page: number pinned: boolean @@ -229,7 +229,7 @@ export const savedSessionRecordingPlaylistsLogic = kea ({ - [urls.replay(ReplayTabs.Playlists)]: async (_, searchParams) => { + [urls.replay(ReplayTabs.Playlists)]: (_, searchParams) => { const currentFilters = values.filters const nextFilters = objectClean(searchParams) if (!objectsEqual(currentFilters, nextFilters)) { diff --git a/frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx b/frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx index 1f7ae8fd46a40..018ba2ba5e589 100644 --- a/frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx +++ b/frontend/src/scenes/settings/organization/Permissions/Roles/rolesLogic.tsx @@ -55,7 +55,7 @@ export const rolesLogic = kea([ }, ], }), - loaders(({ values, actions }) => ({ + loaders(({ values, actions, asyncActions }) => ({ roles: { loadRoles: async () => { const response = await api.roles.list() @@ -64,7 +64,7 @@ export const rolesLogic = kea([ createRole: async (roleName: string) => { const { roles, roleMembersToAdd } = values const newRole = await api.roles.create(roleName) - await actions.addRoleMembers({ role: newRole, membersToAdd: roleMembersToAdd }) + await asyncActions.addRoleMembers({ role: newRole, membersToAdd: roleMembersToAdd }) eventUsageLogic.actions.reportRoleCreated(roleName) actions.setRoleMembersInFocus([]) actions.setRoleMembersToAdd([]) diff --git a/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts index 33eee0d7e5be7..fbc0a829bc5ba 100644 --- a/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts +++ b/frontend/src/scenes/settings/organization/VerifiedDomains/verifiedDomainsLogic.ts @@ -111,7 +111,7 @@ export const verifiedDomainsLogic = kea([ 'We could not verify your domain yet. DNS propagation may take up to 72 hours. Please try again later.' ) } - actions.replaceDomain(response as OrganizationDomainType) + actions.replaceDomain(response) actions.setVerifyModal(null) return false }, diff --git a/frontend/src/scenes/settings/organization/invitesLogic.tsx b/frontend/src/scenes/settings/organization/invitesLogic.tsx index f23a99e1c4bc3..0cb80e1dacca3 100644 --- a/frontend/src/scenes/settings/organization/invitesLogic.tsx +++ b/frontend/src/scenes/settings/organization/invitesLogic.tsx @@ -46,7 +46,7 @@ export const invitesLogic = kea([ }, })), listeners({ - createInviteSuccess: async () => { + createInviteSuccess: () => { const nameProvided = false // TODO: Change when adding support for names on invites eventUsageLogic.actions.reportInviteAttempted( nameProvided, diff --git a/frontend/src/scenes/settings/project/AutocaptureSettings.tsx b/frontend/src/scenes/settings/project/AutocaptureSettings.tsx index 8023a65757846..8e155bbb77323 100644 --- a/frontend/src/scenes/settings/project/AutocaptureSettings.tsx +++ b/frontend/src/scenes/settings/project/AutocaptureSettings.tsx @@ -12,7 +12,7 @@ export function AutocaptureSettings(): JSX.Element { const { userLoading } = useValues(userLogic) const { currentTeam } = useValues(teamLogic) const { updateCurrentTeam } = useActions(teamLogic) - const { reportIngestionAutocaptureToggled } = useActions(eventUsageLogic) + const { reportAutocaptureToggled } = useActions(eventUsageLogic) return ( <> @@ -34,7 +34,7 @@ export function AutocaptureSettings(): JSX.Element { updateCurrentTeam({ autocapture_opt_out: !checked, }) - reportIngestionAutocaptureToggled(!checked) + reportAutocaptureToggled(!checked) }} checked={!currentTeam?.autocapture_opt_out} disabled={userLoading} @@ -50,7 +50,7 @@ export function ExceptionAutocaptureSettings(): JSX.Element { const { userLoading } = useValues(userLogic) const { currentTeam } = useValues(teamLogic) const { updateCurrentTeam } = useActions(teamLogic) - const { reportIngestionAutocaptureExceptionsToggled } = useActions(eventUsageLogic) + const { reportAutocaptureExceptionsToggled } = useActions(eventUsageLogic) const { errorsToIgnoreRules, rulesCharacters } = useValues(autocaptureExceptionsLogic) const { setErrorsToIgnoreRules } = useActions(autocaptureExceptionsLogic) @@ -63,7 +63,7 @@ export function ExceptionAutocaptureSettings(): JSX.Element { updateCurrentTeam({ autocapture_exceptions_opt_in: checked, }) - reportIngestionAutocaptureExceptionsToggled(checked) + reportAutocaptureExceptionsToggled(checked) }} checked={!!currentTeam?.autocapture_exceptions_opt_in} disabled={userLoading} @@ -82,7 +82,7 @@ export function ExceptionAutocaptureSettings(): JSX.Element {

    You can enter a regular expression that matches values of{' '} here to ignore them. One per line. For example, if you - want to drop all errors that contain the word "bot", or you can enter "bot" here. Or if you want to drop + want to drop all errors that contain the word "bot", you can enter "bot" here. Or if you want to drop all errors that are exactly "bot", you can enter "^bot$".

    Only up to 300 characters of config are allowed here.

    diff --git a/frontend/src/scenes/settings/project/groupAnalyticsConfigLogic.ts b/frontend/src/scenes/settings/project/groupAnalyticsConfigLogic.ts index 1049b2299ab7b..3edfe58f5b7d1 100644 --- a/frontend/src/scenes/settings/project/groupAnalyticsConfigLogic.ts +++ b/frontend/src/scenes/settings/project/groupAnalyticsConfigLogic.ts @@ -42,7 +42,7 @@ export const groupAnalyticsConfigLogic = kea([ ], }), listeners(({ values, actions }) => ({ - save: async () => { + save: () => { const { groupTypes, singularChanges, pluralChanges } = values const payload = Array.from(groupTypes.values()).map((groupType) => { const result = { ...groupType } diff --git a/frontend/src/scenes/settings/project/webhookIntegrationLogic.ts b/frontend/src/scenes/settings/project/webhookIntegrationLogic.ts index 7e26d5516701c..b9df2b8de3b72 100644 --- a/frontend/src/scenes/settings/project/webhookIntegrationLogic.ts +++ b/frontend/src/scenes/settings/project/webhookIntegrationLogic.ts @@ -56,7 +56,7 @@ export const webhookIntegrationLogic = kea([ ], }), listeners(() => ({ - testWebhookSuccess: async ({ testedWebhook }) => { + testWebhookSuccess: ({ testedWebhook }) => { if (testedWebhook) { teamLogic.actions.updateCurrentTeam({ slack_incoming_webhook: testedWebhook }) } diff --git a/frontend/src/scenes/settings/settingsLogic.ts b/frontend/src/scenes/settings/settingsLogic.ts index ee42db7d97be8..18d49fb425cd4 100644 --- a/frontend/src/scenes/settings/settingsLogic.ts +++ b/frontend/src/scenes/settings/settingsLogic.ts @@ -93,10 +93,9 @@ export const settingsLogic = kea([ }), listeners(({ values }) => ({ - selectSetting({ setting }) { + async selectSetting({ setting }) { const url = urls.settings(values.selectedSectionId ?? values.selectedLevel, setting as SettingId) - - copyToClipboard(window.location.origin + url) + await copyToClipboard(window.location.origin + url) }, })), ]) diff --git a/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts b/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts index 2da5b9b1a85c3..dc9620e9b34ba 100644 --- a/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts +++ b/frontend/src/scenes/settings/user/personalAPIKeysLogic.ts @@ -26,7 +26,7 @@ export const personalAPIKeysLogic = kea([ }, deleteKey: async (key: PersonalAPIKeyType) => { await api.delete(`api/personal_api_keys/${key.id}/`) - return (values.keys as PersonalAPIKeyType[]).filter((filteredKey) => filteredKey.id != key.id) + return values.keys.filter((filteredKey) => filteredKey.id != key.id) }, }, ], diff --git a/frontend/src/scenes/surveys/SurveyAppearance.tsx b/frontend/src/scenes/surveys/SurveyAppearance.tsx index a0dd1bf2453f9..c98574af938d9 100644 --- a/frontend/src/scenes/surveys/SurveyAppearance.tsx +++ b/frontend/src/scenes/surveys/SurveyAppearance.tsx @@ -107,7 +107,7 @@ export function SurveyAppearance({ surveyQuestionItem.type === SurveyQuestionType.MultipleChoice) && ( undefined} /> diff --git a/frontend/src/scenes/surveys/SurveyEdit.tsx b/frontend/src/scenes/surveys/SurveyEdit.tsx index 26fef43c2e3c1..64d2e0aad64ae 100644 --- a/frontend/src/scenes/surveys/SurveyEdit.tsx +++ b/frontend/src/scenes/surveys/SurveyEdit.tsx @@ -453,7 +453,13 @@ export default function SurveyEdit(): JSX.Element { SurveyQuestionType.MultipleChoice) && (
    - {({ value, onChange }) => ( + {({ + value, + onChange, + }: { + value: string[] + onChange: (newValue: string[]) => void + }) => (
    {(value || []).map( ( @@ -847,7 +853,7 @@ export default function SurveyEdit(): JSX.Element { />
    - + diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index a5d6df1eb12f0..56bd30b709298 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -35,7 +35,8 @@ export enum SurveyEditSection { Targeting = 'targeting', } export interface SurveyLogicProps { - id: string | 'new' + /** Either a UUID or 'new'. */ + id: string } export interface SurveyMetricsQueries { @@ -420,7 +421,7 @@ export const surveyLogic = kea([ actions.loadSurveys() actions.reportSurveyResumed(survey) }, - archiveSurvey: async () => { + archiveSurvey: () => { actions.updateSurvey({ archived: true }) }, loadSurveySuccess: () => { @@ -684,7 +685,7 @@ export const surveyLogic = kea([ // controlled using a PureField in the form urlMatchType: values.urlMatchTypeValidationError, }), - submit: async (surveyPayload) => { + submit: (surveyPayload) => { let surveyPayloadWithTargetingFlagFilters = surveyPayload const flagLogic = featureFlagLogic({ id: values.survey.targeting_flag?.id || 'new' }) if (values.hasTargetingFlag) { @@ -723,12 +724,12 @@ export const surveyLogic = kea([ return [urls.survey(values.survey.id), router.values.searchParams, hashParams] }, })), - afterMount(async ({ props, actions }) => { + afterMount(({ props, actions }) => { if (props.id !== 'new') { - await actions.loadSurvey() + actions.loadSurvey() } if (props.id === 'new') { - await actions.resetSurvey() + actions.resetSurvey() } }), ]) diff --git a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx index cd8005288e48e..894a03d70b6e0 100644 --- a/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx +++ b/frontend/src/scenes/trends/persons-modal/PersonsModal.tsx @@ -185,7 +185,7 @@ export function PersonsModal({ { - triggerExport({ + void triggerExport({ export_format: ExporterFormat.CSV, export_context: { path: originalUrl, diff --git a/frontend/src/scenes/trends/trendsDataLogic.ts b/frontend/src/scenes/trends/trendsDataLogic.ts index 9611b23da1a4e..9ab296c4845e3 100644 --- a/frontend/src/scenes/trends/trendsDataLogic.ts +++ b/frontend/src/scenes/trends/trendsDataLogic.ts @@ -88,7 +88,6 @@ export const trendsDataLogic = kea([ } else if (lifecycleFilter) { if (lifecycleFilter.toggledLifecycles) { indexedResults = indexedResults.filter((result) => - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion lifecycleFilter.toggledLifecycles!.includes(String(result.status) as LifecycleToggle) ) } diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index 6d23338abfca2..ee4f3c26699f2 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -9,6 +9,7 @@ import { DashboardType, FilterType, InsightShortId, + PipelineAppTabs, PipelineTabs, ReplayTabs, } from '~/types' @@ -97,8 +98,10 @@ export const urls = { personByUUID: (uuid: string, encode: boolean = true): string => encode ? `/persons/${encodeURIComponent(uuid)}` : `/persons/${uuid}`, persons: (): string => '/persons', - pipeline: (tab?: PipelineTabs): string => `/pipeline/${tab ? tab : 'destinations'}`, - pipelineNew: (tab?: PipelineTabs): string => `/pipeline/${tab ? tab : 'destinations'}/new`, + pipeline: (tab?: PipelineTabs): string => `/pipeline/${tab ? tab : PipelineTabs.Destinations}`, + pipelineApp: (id: string | number, tab?: PipelineAppTabs): string => + `/pipeline/${id}/${tab ? tab : PipelineAppTabs.Configuration}`, + pipelineNew: (tab?: PipelineTabs): string => `/pipeline/${tab ? tab : PipelineTabs.Destinations}/new`, groups: (groupTypeIndex: string | number): string => `/groups/${groupTypeIndex}`, // :TRICKY: Note that groupKey is provided by user. We need to override urlPatternOptions for kea-router. group: (groupTypeIndex: string | number, groupKey: string, encode: boolean = true, tab?: string | null): string => @@ -110,9 +113,11 @@ export const urls = { featureFlags: (tab?: string): string => `/feature_flags${tab ? `?tab=${tab}` : ''}`, featureFlag: (id: string | number): string => `/feature_flags/${id}`, earlyAccessFeatures: (): string => '/early_access_features', - earlyAccessFeature: (id: ':id' | 'new' | string): string => `/early_access_features/${id}`, + /** @param id A UUID or 'new'. ':id' for routing. */ + earlyAccessFeature: (id: string): string => `/early_access_features/${id}`, surveys: (): string => '/surveys', - survey: (id: ':id' | 'new' | string): string => `/surveys/${id}`, + /** @param id A UUID or 'new'. ':id' for routing. */ + survey: (id: string): string => `/surveys/${id}`, surveyTemplates: (): string => '/survey_templates', dataWarehouse: (): string => '/data-warehouse', dataWarehouseTable: (): string => `/data-warehouse/new`, @@ -154,7 +159,6 @@ export const urls = { verifyEmail: (userUuid: string = '', token: string = ''): string => `/verify_email${userUuid ? `/${userUuid}` : ''}${token ? `/${token}` : ''}`, inviteSignup: (id: string): string => `/signup/${id}`, - ingestion: (): string => '/ingestion', products: (): string => '/products', onboarding: (productKey: string): string => `/onboarding/${productKey}`, // Cloud only diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx index 8ca552e509458..460c02797108f 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx +++ b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx @@ -1,8 +1,8 @@ -import { useActions } from 'kea' +import { useActions, useValues } from 'kea' import { UnexpectedNeverError } from 'lib/utils' import { useCallback, useMemo } from 'react' import { countryCodeToFlag, countryCodeToName } from 'scenes/insights/views/WorldMap' -import { webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' +import { GeographyTab, webAnalyticsLogic } from 'scenes/web-analytics/webAnalyticsLogic' import { Query } from '~/queries/Query/Query' import { DataTableNode, InsightVizNode, NodeKind, WebStatsBreakdown } from '~/queries/schema' @@ -174,11 +174,16 @@ export const webAnalyticsDataTableQueryContext: QueryContext = { } export const WebStatsTrendTile = ({ query }: { query: InsightVizNode }): JSX.Element => { - const { togglePropertyFilter } = useActions(webAnalyticsLogic) + const { togglePropertyFilter, setGeographyTab } = useActions(webAnalyticsLogic) + const { hasCountryFilter } = useValues(webAnalyticsLogic) const { key: worldMapPropertyName } = webStatsBreakdownToPropertyName(WebStatsBreakdown.Country) const onWorldMapClick = useCallback( (breakdownValue: string) => { togglePropertyFilter(PropertyFilterType.Event, worldMapPropertyName, breakdownValue) + if (!hasCountryFilter) { + // if we just added a country filter, switch to the region tab, as the world map will not be useful + setGeographyTab(GeographyTab.REGIONS) + } }, [togglePropertyFilter, worldMapPropertyName] ) @@ -188,9 +193,14 @@ export const WebStatsTrendTile = ({ query }: { query: InsightVizNode }): JSX.Ele ...webAnalyticsDataTableQueryContext, chartRenderingMetadata: { [ChartDisplayType.WorldMap]: { - countryProps: (countryCode, values) => ({ - onClick: values && values.count > 0 ? () => onWorldMapClick(countryCode) : undefined, - }), + countryProps: (countryCode, values) => { + return { + onClick: + values && (values.count > 0 || values.aggregated_value > 0) + ? () => onWorldMapClick(countryCode) + : undefined, + } + }, }, }, } diff --git a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts index adc48ef95fcd3..23b4450b4ae6f 100644 --- a/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts +++ b/frontend/src/scenes/web-analytics/webAnalyticsLogic.ts @@ -19,6 +19,7 @@ import { EventDefinition, EventDefinitionType, InsightType, + PropertyDefinition, PropertyFilterType, PropertyOperator, RetentionPeriod, @@ -225,6 +226,7 @@ export const webAnalyticsLogic = kea([ s.dateFrom, s.dateTo, () => values.isGreaterThanMd, + () => values.shouldShowGeographyTile, ], ( webAnalyticsFilters, @@ -235,13 +237,14 @@ export const webAnalyticsLogic = kea([ geographyTab, dateFrom, dateTo, - isGreaterThanMd: boolean + isGreaterThanMd: boolean, + shouldShowGeographyTile ): WebDashboardTile[] => { const dateRange = { date_from: dateFrom, date_to: dateTo, } - return [ + const tiles: (WebDashboardTile | null)[] = [ { layout: { colSpan: 12, @@ -493,89 +496,6 @@ export const webAnalyticsLogic = kea([ }, ], }, - { - layout: { - colSpan: 6, - }, - activeTabId: geographyTab, - setTabId: actions.setGeographyTab, - tabs: [ - { - id: GeographyTab.MAP, - title: 'World map', - linkText: 'Map', - query: { - kind: NodeKind.InsightVizNode, - source: { - kind: NodeKind.TrendsQuery, - breakdown: { - breakdown: '$geoip_country_code', - breakdown_type: 'person', - }, - dateRange, - series: [ - { - event: '$pageview', - kind: NodeKind.EventsNode, - math: BaseMathType.UniqueUsers, - }, - ], - trendsFilter: { - display: ChartDisplayType.WorldMap, - }, - filterTestAccounts: true, - properties: webAnalyticsFilters, - }, - hidePersonsModal: true, - }, - }, - { - id: GeographyTab.COUNTRIES, - title: 'Top countries', - linkText: 'Countries', - query: { - full: true, - kind: NodeKind.DataTableNode, - source: { - kind: NodeKind.WebStatsTableQuery, - properties: webAnalyticsFilters, - breakdownBy: WebStatsBreakdown.Country, - dateRange, - }, - }, - }, - { - id: GeographyTab.REGIONS, - title: 'Top regions', - linkText: 'Regions', - query: { - full: true, - kind: NodeKind.DataTableNode, - source: { - kind: NodeKind.WebStatsTableQuery, - properties: webAnalyticsFilters, - breakdownBy: WebStatsBreakdown.Region, - dateRange, - }, - }, - }, - { - id: GeographyTab.CITIES, - title: 'Top cities', - linkText: 'Cities', - query: { - full: true, - kind: NodeKind.DataTableNode, - source: { - kind: NodeKind.WebStatsTableQuery, - properties: webAnalyticsFilters, - breakdownBy: WebStatsBreakdown.City, - dateRange, - }, - }, - }, - ], - }, { title: 'Retention', layout: { @@ -604,7 +524,99 @@ export const webAnalyticsLogic = kea([ }, }, }, + shouldShowGeographyTile + ? { + layout: { + colSpan: 12, + }, + activeTabId: geographyTab, + setTabId: actions.setGeographyTab, + tabs: [ + { + id: GeographyTab.MAP, + title: 'World map', + linkText: 'Map', + query: { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + breakdown: { + breakdown: '$geoip_country_code', + breakdown_type: 'person', + }, + dateRange, + series: [ + { + event: '$pageview', + kind: NodeKind.EventsNode, + math: BaseMathType.UniqueUsers, + }, + ], + trendsFilter: { + display: ChartDisplayType.WorldMap, + }, + filterTestAccounts: true, + properties: webAnalyticsFilters, + }, + hidePersonsModal: true, + }, + }, + { + id: GeographyTab.COUNTRIES, + title: 'Top countries', + linkText: 'Countries', + query: { + full: true, + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.WebStatsTableQuery, + properties: webAnalyticsFilters, + breakdownBy: WebStatsBreakdown.Country, + dateRange, + }, + }, + }, + { + id: GeographyTab.REGIONS, + title: 'Top regions', + linkText: 'Regions', + query: { + full: true, + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.WebStatsTableQuery, + properties: webAnalyticsFilters, + breakdownBy: WebStatsBreakdown.Region, + dateRange, + }, + }, + }, + { + id: GeographyTab.CITIES, + title: 'Top cities', + linkText: 'Cities', + query: { + full: true, + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.WebStatsTableQuery, + properties: webAnalyticsFilters, + breakdownBy: WebStatsBreakdown.City, + dateRange, + }, + }, + }, + ], + } + : null, ] + return tiles.filter(isNotNil) + }, + ], + hasCountryFilter: [ + (s) => [s.webAnalyticsFilters], + (webAnalyticsFilters: WebAnalyticsPropertyFilters) => { + return webAnalyticsFilters.some((filter) => filter.key === '$geoip_country_code') }, ], })), @@ -637,8 +649,8 @@ export const webAnalyticsLogic = kea([ ? pageleaveResult.value.results.find((r) => r.name === '$pageleave') : undefined - const shouldWarnAboutNoPageviews = !pageviewEntry || isEventDefinitionStale(pageviewEntry) - const shouldWarnAboutNoPageleaves = !pageleaveEntry || isEventDefinitionStale(pageleaveEntry) + const shouldWarnAboutNoPageviews = !pageviewEntry || isDefinitionStale(pageviewEntry) + const shouldWarnAboutNoPageleaves = !pageleaveEntry || isDefinitionStale(pageleaveEntry) return { shouldWarnAboutNoPageviews, @@ -646,18 +658,30 @@ export const webAnalyticsLogic = kea([ } }, }, + shouldShowGeographyTile: { + _default: null as boolean | null, + loadShouldShowGeographyTile: async (): Promise => { + const response = await api.propertyDefinitions.list({ + event_names: ['$pageview'], + properties: ['$geoip_country_code'], + }) + const countryCodeDefinition = response.results.find((r) => r.name === '$geoip_country_code') + return !!countryCodeDefinition && !isDefinitionStale(countryCodeDefinition) + }, + }, })), // start the loaders after mounting the logic afterMount(({ actions }) => { actions.loadStatusCheck() + actions.loadShouldShowGeographyTile() }), windowValues({ isGreaterThanMd: (window: Window) => window.innerWidth > 768, }), ]) -const isEventDefinitionStale = (definition: EventDefinition): boolean => { +const isDefinitionStale = (definition: EventDefinition | PropertyDefinition): boolean => { const parsedLastSeen = definition.last_seen_at ? dayjs(definition.last_seen_at) : null return !!parsedLastSeen && dayjs().diff(parsedLastSeen, 'seconds') > STALE_EVENT_SECONDS } diff --git a/frontend/src/toolbar/actions/actionsLogic.ts b/frontend/src/toolbar/actions/actionsLogic.ts index 72007e0209d3e..68c101a7fb532 100644 --- a/frontend/src/toolbar/actions/actionsLogic.ts +++ b/frontend/src/toolbar/actions/actionsLogic.ts @@ -64,9 +64,7 @@ export const actionsLogic = kea([ .search(searchTerm) .map(({ item }) => item) : allActions - return [...filteredActions].sort((a, b) => - (a.name ?? 'Untitled').localeCompare(b.name ?? 'Untitled') - ) as ActionType[] + return [...filteredActions].sort((a, b) => (a.name ?? 'Untitled').localeCompare(b.name ?? 'Untitled')) }, ], actionCount: [(s) => [s.allActions], (allActions) => allActions.length], diff --git a/frontend/src/toolbar/elements/heatmapLogic.ts b/frontend/src/toolbar/elements/heatmapLogic.ts index 82e80fa1bfac1..36bf9d52ddb27 100644 --- a/frontend/src/toolbar/elements/heatmapLogic.ts +++ b/frontend/src/toolbar/elements/heatmapLogic.ts @@ -186,7 +186,7 @@ export const heatmapLogic = kea([ if (domElements === undefined) { domElements = Array.from( querySelectorAllDeep(combinedSelector, document, cache.pageElements) - ) as HTMLElement[] + ) cache.selectorToElements[combinedSelector] = domElements } diff --git a/frontend/src/toolbar/flags/featureFlagsLogic.ts b/frontend/src/toolbar/flags/featureFlagsLogic.ts index 475ed1ecd560c..a6e21b5458aab 100644 --- a/frontend/src/toolbar/flags/featureFlagsLogic.ts +++ b/frontend/src/toolbar/flags/featureFlagsLogic.ts @@ -113,7 +113,7 @@ export const featureFlagsLogic = kea([ toolbarLogic.values.posthog?.featureFlags.reloadFeatureFlags() } }, - deleteOverriddenUserFlag: async ({ flagKey }) => { + deleteOverriddenUserFlag: ({ flagKey }) => { const { posthog: clientPostHog } = toolbarLogic.values if (clientPostHog) { const updatedFlags = { ...values.localOverrides } @@ -130,8 +130,8 @@ export const featureFlagsLogic = kea([ }, })), events(({ actions }) => ({ - afterMount: async () => { - await actions.getUserFlags() + afterMount: () => { + actions.getUserFlags() actions.checkLocalOverrides() }, })), diff --git a/frontend/src/toolbar/toolbarLogic.ts b/frontend/src/toolbar/toolbarLogic.ts index 42aa80199c11f..1394e1d1d05f2 100644 --- a/frontend/src/toolbar/toolbarLogic.ts +++ b/frontend/src/toolbar/toolbarLogic.ts @@ -1,6 +1,5 @@ import { actions, afterMount, kea, listeners, path, props, reducers, selectors } from 'kea' import { lemonToast } from 'lib/lemon-ui/lemonToast' -import type { PostHog } from 'posthog-js' import { actionsTabLogic } from '~/toolbar/actions/actionsTabLogic' import { toolbarButtonLogic } from '~/toolbar/button/toolbarButtonLogic' @@ -32,8 +31,8 @@ export const toolbarLogic = kea([ userIntent: [props.userIntent || null, { logout: () => null, clearUserIntent: () => null }], source: [props.source || null, { logout: () => null }], buttonVisible: [true, { showButton: () => true, hideButton: () => false, logout: () => false }], - dataAttributes: [(props.dataAttributes || []) as string[]], - posthog: [(props.posthog ?? null) as PostHog | null], + dataAttributes: [props.dataAttributes || []], + posthog: [props.posthog ?? null], })), selectors({ @@ -65,7 +64,7 @@ export const toolbarLogic = kea([ } clearSessionToolbarToken() }, - processUserIntent: async () => { + processUserIntent: () => { if (props.userIntent === 'add-action' || props.userIntent === 'edit-action') { actionsTabLogic.actions.showButtonActions() toolbarButtonLogic.actions.showActionsInfo() diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 1ed4fa04eb48d..d595f24fd0d50 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -527,6 +527,12 @@ export enum PipelineTabs { Destinations = 'destinations', } +export enum PipelineAppTabs { + Configuration = 'configuration', + Logs = 'logs', + Metrics = 'metrics', +} + export enum ProgressStatus { Draft = 'draft', Running = 'running', @@ -1273,6 +1279,7 @@ export interface BillingV2PlanType { current_plan?: any tiers?: BillingV2TierType[] included_if?: 'no_active_subscription' | 'has_subscription' | null + initial_billing_limit?: number } export interface PlanInterface { @@ -1342,7 +1349,7 @@ export interface InsightModel extends Cacheable { description?: string favorited?: boolean order: number | null - result: any | null + result: any deleted: boolean saved: boolean created_at: string diff --git a/latest_migrations.manifest b/latest_migrations.manifest index 83decc1fa7bd1..7ad1758c3c617 100644 --- a/latest_migrations.manifest +++ b/latest_migrations.manifest @@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name ee: 0015_add_verified_properties otp_static: 0002_throttling otp_totp: 0002_auto_20190420_0723 -posthog: 0363_add_replay_payload_capture_config +posthog: 0364_team_external_data_workspace_rows sessions: 0001_initial social_django: 0010_uid_db_index two_factor: 0007_auto_20201201_1019 diff --git a/plugin-server/src/utils/status.ts b/plugin-server/src/utils/status.ts index d620bd01b92c6..385b97739685e 100644 --- a/plugin-server/src/utils/status.ts +++ b/plugin-server/src/utils/status.ts @@ -1,5 +1,6 @@ import pino from 'pino' +import { defaultConfig } from '../config/config' import { LogLevel, PluginsServerConfig } from '../types' import { isProdEnv } from './env-utils' @@ -14,7 +15,6 @@ export interface StatusBlueprint { export class Status implements StatusBlueprint { mode?: string - explicitLogLevel?: LogLevel logger: pino.Logger prompt: string transport: any @@ -22,7 +22,7 @@ export class Status implements StatusBlueprint { constructor(mode?: string) { this.mode = mode - const logLevel: LogLevel = this.explicitLogLevel || LogLevel.Info + const logLevel: LogLevel = defaultConfig.LOG_LEVEL if (isProdEnv()) { this.logger = pino({ // By default pino will log the level number. So we can easily unify diff --git a/plugin-server/src/worker/plugins/run.ts b/plugin-server/src/worker/plugins/run.ts index 9775e9c1ef860..e957132313168 100644 --- a/plugin-server/src/worker/plugins/run.ts +++ b/plugin-server/src/worker/plugins/run.ts @@ -111,7 +111,7 @@ async function runSingleTeamPluginComposeWebhook( const request = await trackedFetch(webhook.url, { method: webhook.method || 'POST', body: JSON.stringify(webhook.body, undefined, 4), - headers: { 'Content-Type': 'application/json' }, + headers: webhook.headers || { 'Content-Type': 'application/json' }, timeout: hub.EXTERNAL_REQUEST_TIMEOUT_MS, }) if (request.ok) { diff --git a/posthog/api/capture.py b/posthog/api/capture.py index a7d72f9ca1f3e..ac954a3b8d6d2 100644 --- a/posthog/api/capture.py +++ b/posthog/api/capture.py @@ -262,11 +262,11 @@ def drop_events_over_quota(token: str, events: List[Any]) -> List[Any]: if not settings.EE_AVAILABLE: return events - from ee.billing.quota_limiting import QuotaResource, list_limited_team_tokens + from ee.billing.quota_limiting import QuotaResource, list_limited_team_attributes results = [] - limited_tokens_events = list_limited_team_tokens(QuotaResource.EVENTS) - limited_tokens_recordings = list_limited_team_tokens(QuotaResource.RECORDINGS) + limited_tokens_events = list_limited_team_attributes(QuotaResource.EVENTS) + limited_tokens_recordings = list_limited_team_attributes(QuotaResource.RECORDINGS) for event in events: if event.get("event") in SESSION_RECORDING_EVENT_NAMES: diff --git a/posthog/api/organization_feature_flag.py b/posthog/api/organization_feature_flag.py index 6f339f2976a5a..d149de721dccb 100644 --- a/posthog/api/organization_feature_flag.py +++ b/posthog/api/organization_feature_flag.py @@ -1,8 +1,4 @@ -from posthog.api.routing import StructuredViewSetMixin -from posthog.api.feature_flag import FeatureFlagSerializer -from posthog.api.feature_flag import CanEditFeatureFlag -from posthog.models import FeatureFlag, Team -from posthog.permissions import OrganizationMemberPermissions +from typing import Dict from django.core.exceptions import ObjectDoesNotExist from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated @@ -12,6 +8,14 @@ viewsets, status, ) +from posthog.api.cohort import CohortSerializer +from posthog.api.routing import StructuredViewSetMixin +from posthog.api.feature_flag import FeatureFlagSerializer +from posthog.api.feature_flag import CanEditFeatureFlag +from posthog.models import FeatureFlag, Team +from posthog.models.cohort import Cohort +from posthog.models.filters.filter import Filter +from posthog.permissions import OrganizationMemberPermissions class OrganizationFeatureFlagView( @@ -86,7 +90,7 @@ def copy_flags(self, request, *args, **kwargs): for target_project_id in target_project_ids: # Target project does not exist try: - Team.objects.get(id=target_project_id) + target_project = Team.objects.get(id=target_project_id) except ObjectDoesNotExist: failed_projects.append( { @@ -96,10 +100,65 @@ def copy_flags(self, request, *args, **kwargs): ) continue - context = { - "request": request, - "team_id": target_project_id, - } + # get all linked cohorts, sorted by creation order + seen_cohorts_cache: Dict[str, Cohort] = {} + sorted_cohort_ids = flag_to_copy.get_cohort_ids( + seen_cohorts_cache=seen_cohorts_cache, sort_by_topological_order=True + ) + + # destination cohort id is different from original cohort id - create mapping + name_to_dest_cohort_id: Dict[str, int] = {} + # create cohorts in the destination project + if len(sorted_cohort_ids): + for cohort_id in sorted_cohort_ids: + original_cohort = seen_cohorts_cache[str(cohort_id)] + + # search in destination project by name + destination_cohort = Cohort.objects.filter( + name=original_cohort.name, team_id=target_project_id, deleted=False + ).first() + + # create new cohort in the destination project + if not destination_cohort: + prop_group = Filter( + data={"properties": original_cohort.properties.to_dict(), "is_simplified": True} + ).property_groups + + for prop in prop_group.flat: + if prop.type == "cohort": + original_child_cohort_id = prop.value + original_child_cohort = seen_cohorts_cache[str(original_child_cohort_id)] + prop.value = name_to_dest_cohort_id[original_child_cohort.name] + + destination_cohort_serializer = CohortSerializer( + data={ + "team": target_project, + "name": original_cohort.name, + "groups": [], + "filters": {"properties": prop_group.to_dict()}, + "description": original_cohort.description, + "is_static": original_cohort.is_static, + }, + context={ + "request": request, + "team_id": target_project.id, + }, + ) + destination_cohort_serializer.is_valid(raise_exception=True) + destination_cohort = destination_cohort_serializer.save() + + if destination_cohort is not None: + name_to_dest_cohort_id[original_cohort.name] = destination_cohort.id + + # reference correct destination cohort ids in the flag + for group in flag_to_copy.conditions: + props = group.get("properties", []) + for prop in props: + if isinstance(prop, dict) and prop.get("type") == "cohort": + original_cohort_id = prop["value"] + cohort_name = (seen_cohorts_cache[str(original_cohort_id)]).name + prop["value"] = name_to_dest_cohort_id[cohort_name] + flag_data = { "key": flag_to_copy.key, "name": flag_to_copy.name, @@ -109,6 +168,10 @@ def copy_flags(self, request, *args, **kwargs): "ensure_experience_continuity": flag_to_copy.ensure_experience_continuity, "deleted": False, } + context = { + "request": request, + "team_id": target_project_id, + } existing_flag = FeatureFlag.objects.filter( key=feature_flag_key, team_id=target_project_id, deleted=False diff --git a/posthog/api/survey.py b/posthog/api/survey.py index a2b3e8c3fcdd3..ef3e8c166dac8 100644 --- a/posthog/api/survey.py +++ b/posthog/api/survey.py @@ -221,19 +221,29 @@ def update(self, instance: Survey, validated_data): existing_flag_serializer.is_valid(raise_exception=True) existing_flag_serializer.save() else: - new_flag = self._create_new_targeting_flag(instance.name, new_filters) + new_flag = self._create_new_targeting_flag(instance.name, new_filters, bool(instance.start_date)) validated_data["targeting_flag_id"] = new_flag.id validated_data.pop("targeting_flag_filters") + end_date = validated_data.get("end_date") + if instance.targeting_flag: + # turn off feature flag if survey is ended + if end_date is None: + instance.targeting_flag.active = True + else: + instance.targeting_flag.active = False + instance.targeting_flag.save() + return super().update(instance, validated_data) - def _create_new_targeting_flag(self, name, filters): + def _create_new_targeting_flag(self, name, filters, active=False): feature_flag_key = slugify(f"{SURVEY_TARGETING_FLAG_PREFIX}{name}") feature_flag_serializer = FeatureFlagSerializer( data={ "key": feature_flag_key, "name": f"Targeting flag for survey {name}", "filters": filters, + "active": active, }, context=self.context, ) diff --git a/posthog/api/test/__snapshots__/test_action.ambr b/posthog/api/test/__snapshots__/test_action.ambr index 66a6b7ac1190e..e09dabc5bf688 100644 --- a/posthog/api/test/__snapshots__/test_action.ambr +++ b/posthog/api/test/__snapshots__/test_action.ambr @@ -71,7 +71,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_actions-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/actions/%3F%24'*/ @@ -226,7 +227,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_actions-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/actions/%3F%24'*/ @@ -552,7 +554,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_actions-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/actions/%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_annotation.ambr b/posthog/api/test/__snapshots__/test_annotation.ambr index 50d15b6145259..1373bf5f4060b 100644 --- a/posthog/api/test/__snapshots__/test_annotation.ambr +++ b/posthog/api/test/__snapshots__/test_annotation.ambr @@ -71,7 +71,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_annotations-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/annotations/%3F%24'*/ @@ -150,7 +151,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_annotations-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/annotations/%3F%24'*/ @@ -474,7 +476,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_annotations-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/annotations/%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index c11bcf8af7d75..655c44eff5ab9 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -81,7 +81,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -306,7 +307,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -461,7 +463,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -604,7 +607,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."api_token" = 'token123' LIMIT 21 diff --git a/posthog/api/test/__snapshots__/test_early_access_feature.ambr b/posthog/api/test/__snapshots__/test_early_access_feature.ambr index 5328b0eaa8e62..d7908693f9cb2 100644 --- a/posthog/api/test/__snapshots__/test_early_access_feature.ambr +++ b/posthog/api/test/__snapshots__/test_early_access_feature.ambr @@ -49,7 +49,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -174,7 +175,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."api_token" = 'token123' LIMIT 21 /*controller='posthog.api.early_access_feature.early_access_features',route='%5Eapi/early_access_features/%3F%28%3F%3A%5B%3F%23%5D.%2A%29%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_element.ambr b/posthog/api/test/__snapshots__/test_element.ambr index 2f1e429bac578..97b03322b3e2d 100644 --- a/posthog/api/test/__snapshots__/test_element.ambr +++ b/posthog/api/test/__snapshots__/test_element.ambr @@ -78,7 +78,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='element-stats',route='api/element/stats/%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_insight.ambr b/posthog/api/test/__snapshots__/test_insight.ambr index 0f8b2b5457332..4d3b882aa5b51 100644 --- a/posthog/api/test/__snapshots__/test_insight.ambr +++ b/posthog/api/test/__snapshots__/test_insight.ambr @@ -669,7 +669,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -719,7 +720,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -852,7 +854,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1075,7 +1078,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1225,6 +1229,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -1352,6 +1357,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -1460,6 +1466,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -1595,7 +1602,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1687,7 +1695,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1771,7 +1780,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1828,7 +1838,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr index b0b05656ef376..e2b852a604b20 100644 --- a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr @@ -125,7 +125,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -221,7 +222,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -313,7 +315,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -515,7 +518,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -648,7 +652,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -787,7 +792,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -879,7 +885,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -1085,7 +1092,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -1211,7 +1219,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -1268,7 +1277,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -1414,7 +1424,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/ @@ -1675,7 +1686,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."organization_id" = '00000000-0000-0000-0000-000000000000'::uuid /*controller='organization_feature_flags-detail',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/%28%3FP%3Cfeature_flag_key%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' diff --git a/posthog/api/test/__snapshots__/test_preflight.ambr b/posthog/api/test/__snapshots__/test_preflight.ambr index 2d2cb9a03cbfe..dcd94e83ea36c 100644 --- a/posthog/api/test/__snapshots__/test_preflight.ambr +++ b/posthog/api/test/__snapshots__/test_preflight.ambr @@ -89,7 +89,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='posthog.views.preflight_check',route='%5E_preflight/%3F%28%3F%3A%5B%3F%23%5D.%2A%29%3F%24'*/ diff --git a/posthog/api/test/__snapshots__/test_survey.ambr b/posthog/api/test/__snapshots__/test_survey.ambr index 4536dfb45977e..1d5a134d8111f 100644 --- a/posthog/api/test/__snapshots__/test_survey.ambr +++ b/posthog/api/test/__snapshots__/test_survey.ambr @@ -150,7 +150,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."api_token" = 'token123' LIMIT 21 /*controller='posthog.api.survey.surveys',route='%5Eapi/surveys/%3F%28%3F%3A%5B%3F%23%5D.%2A%29%3F%24'*/ diff --git a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr index c5475d4515b21..08769c536599c 100644 --- a/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr +++ b/posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr @@ -71,7 +71,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -196,7 +197,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -324,6 +326,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -535,6 +538,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -701,6 +705,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -879,6 +884,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -1043,6 +1049,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -1281,7 +1288,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1338,7 +1346,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1486,7 +1495,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1603,7 +1613,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1660,7 +1671,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1806,7 +1818,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -1939,7 +1952,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -2192,7 +2206,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -2432,7 +2447,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -2568,6 +2584,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -2705,7 +2722,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -2817,7 +2835,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -2923,7 +2942,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -3073,7 +3093,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -3170,6 +3191,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -3292,7 +3314,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -3408,7 +3431,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -3549,7 +3573,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -3862,7 +3887,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -4020,7 +4046,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4154,7 +4181,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4246,7 +4274,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4407,7 +4436,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4464,7 +4494,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4580,7 +4611,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -4737,7 +4769,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5155,7 +5188,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5296,7 +5330,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5388,7 +5423,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5511,7 +5547,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -5595,7 +5632,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5652,7 +5690,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5768,7 +5807,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -5915,7 +5955,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -6078,6 +6119,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -6477,7 +6519,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -6634,6 +6677,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -6815,6 +6859,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -6981,6 +7026,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -7112,7 +7158,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -7209,6 +7256,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -7373,6 +7421,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -8005,7 +8054,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8273,7 +8323,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8434,7 +8485,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8491,7 +8543,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8607,7 +8660,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8764,7 +8818,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -8887,7 +8942,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -9015,7 +9071,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -9162,7 +9219,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -9471,7 +9529,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -9618,7 +9677,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -9722,7 +9782,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ @@ -9859,6 +9920,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -9977,7 +10039,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%28%3FP%3Cpk%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -10116,6 +10179,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -10297,6 +10361,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -10446,7 +10511,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -10557,6 +10623,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -10709,7 +10776,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -10894,7 +10962,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -11005,6 +11074,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -11157,7 +11227,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ @@ -11304,6 +11375,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_organization"."id", "posthog_organization"."name", "posthog_organization"."slug", @@ -11531,7 +11603,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_dashboards-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/dashboards/%3F%24'*/ diff --git a/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr b/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr index fc373eefb7a43..32ff35e826dd3 100644 --- a/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr +++ b/posthog/api/test/notebooks/__snapshots__/test_notebook.ambr @@ -71,7 +71,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/%3F%24'*/ @@ -168,7 +169,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/%28%3FP%3Cshort_id%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -225,7 +227,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/%28%3FP%3Cshort_id%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -334,7 +337,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-all-activity',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/activity/%3F%24'*/ @@ -546,7 +550,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/%28%3FP%3Cshort_id%3E%5B%5E/.%5D%2B%29/%3F%24'*/ @@ -657,6 +662,7 @@ "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at", "posthog_user"."id", "posthog_user"."password", "posthog_user"."last_login", @@ -763,7 +769,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_notebooks-detail',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/notebooks/%28%3FP%3Cshort_id%3E%5B%5E/.%5D%2B%29/%3F%24'*/ diff --git a/posthog/api/test/test_feature_flag_utils.py b/posthog/api/test/test_feature_flag_utils.py new file mode 100644 index 0000000000000..dd6108d7ff54c --- /dev/null +++ b/posthog/api/test/test_feature_flag_utils.py @@ -0,0 +1,73 @@ +from typing import Dict, Set +from posthog.test.base import ( + APIBaseTest, +) +from posthog.models.cohort import Cohort +from posthog.models.cohort.util import sort_cohorts_topologically + + +class TestFeatureFlagUtils(APIBaseTest): + def setUp(self): + super().setUp() + + def test_cohorts_sorted_topologically(self): + cohorts = {} + + def create_cohort(name): + cohorts[name] = Cohort.objects.create( + team=self.team, + name=name, + filters={ + "properties": { + "type": "AND", + "values": [ + {"key": "name", "value": "test", "type": "person"}, + ], + } + }, + ) + + create_cohort("a") + create_cohort("b") + create_cohort("c") + + # (c)-->(b) + cohorts["c"].filters["properties"]["values"][0] = { + "key": "id", + "value": cohorts["b"].pk, + "type": "cohort", + "negation": True, + } + cohorts["c"].save() + + # (a)-->(c) + cohorts["a"].filters["properties"]["values"][0] = { + "key": "id", + "value": cohorts["c"].pk, + "type": "cohort", + "negation": True, + } + cohorts["a"].save() + + cohort_ids = {cohorts["a"].pk, cohorts["b"].pk, cohorts["c"].pk} + seen_cohorts_cache = { + str(cohorts["a"].pk): cohorts["a"], + str(cohorts["b"].pk): cohorts["b"], + str(cohorts["c"].pk): cohorts["c"], + } + + # (a)-->(c)-->(b) + # create b first, since it doesn't depend on any other cohorts + # then c, because it depends on b + # then a, because it depends on c + + # thus destination creation order: b, c, a + destination_creation_order = [cohorts["b"].pk, cohorts["c"].pk, cohorts["a"].pk] + topologically_sorted_cohort_ids = sort_cohorts_topologically(cohort_ids, seen_cohorts_cache) + self.assertEqual(topologically_sorted_cohort_ids, destination_creation_order) + + def test_empty_cohorts_set(self): + cohort_ids: Set[int] = set() + seen_cohorts_cache: Dict[str, Cohort] = {} + topologically_sorted_cohort_ids = sort_cohorts_topologically(cohort_ids, seen_cohorts_cache) + self.assertEqual(topologically_sorted_cohort_ids, []) diff --git a/posthog/api/test/test_organization_feature_flag.py b/posthog/api/test/test_organization_feature_flag.py index cd78e5c238f20..103756d0c4911 100644 --- a/posthog/api/test/test_organization_feature_flag.py +++ b/posthog/api/test/test_organization_feature_flag.py @@ -1,6 +1,8 @@ from rest_framework import status +from posthog.models.cohort.util import sort_cohorts_topologically from posthog.models.user import User from posthog.models.team.team import Team +from posthog.models.cohort import Cohort from ee.models.organization_resource_access import OrganizationResourceAccess from posthog.constants import AvailableFeature from posthog.models import FeatureFlag @@ -428,3 +430,230 @@ def test_copy_feature_flag_cannot_edit(self): } response = self.client.post(url, data) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_copy_feature_flag_cohort_nonexistent_in_destination(self): + cohorts = {} + creation_order = [] + + def create_cohort(name, children): + creation_order.append(name) + properties = [{"key": "$some_prop", "value": "nomatchihope", "type": "person"}] + if children: + properties = [{"key": "id", "type": "cohort", "value": child.pk} for child in children] + + cohorts[name] = Cohort.objects.create( + team=self.team, + name=str(name), + filters={ + "properties": { + "type": "AND", + "values": properties, + } + }, + ) + + # link cohorts + create_cohort(1, None) + create_cohort(3, None) + create_cohort(2, [cohorts[1]]) + create_cohort(4, [cohorts[2], cohorts[3]]) + create_cohort(5, [cohorts[4]]) + create_cohort(6, None) + create_cohort(7, [cohorts[5], cohorts[6]]) # "head" cohort + + flag_to_copy = FeatureFlag.objects.create( + team=self.team_1, + created_by=self.user, + key="flag-with-cohort", + filters={ + "groups": [ + { + "rollout_percentage": 20, + "properties": [ + { + "key": "id", + "type": "cohort", + "value": cohorts[7].pk, # link "head" cohort + } + ], + } + ] + }, + ) + + url = f"/api/organizations/{self.organization.id}/feature_flags/copy_flags" + target_project = self.team_2 + + data = { + "feature_flag_key": flag_to_copy.key, + "from_project": flag_to_copy.team_id, + "target_project_ids": [target_project.id], + } + response = self.client.post(url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # check all cohorts were created in the destination project + for name in creation_order: + found_cohort = Cohort.objects.filter(name=str(name), team_id=target_project.id).exists() + self.assertTrue(found_cohort) + + def test_copy_feature_flag_cohort_nonexistent_in_destination_2(self): + feature_flag_key = "flag-with-cohort" + cohorts = {} + + def create_cohort(name): + cohorts[name] = Cohort.objects.create( + team=self.team, + name=name, + filters={ + "properties": { + "type": "AND", + "values": [ + {"key": "name", "value": "test", "type": "person"}, + ], + } + }, + ) + + create_cohort("a") + create_cohort("b") + create_cohort("c") + create_cohort("d") + + def connect(parent, child): + cohorts[parent].filters["properties"]["values"][0] = { + "key": "id", + "value": cohorts[child].pk, + "type": "cohort", + } + cohorts[parent].save() + + connect("d", "b") + connect("a", "d") + connect("c", "a") + + head_cohort = cohorts["c"] + flag_to_copy = FeatureFlag.objects.create( + team=self.team_1, + created_by=self.user, + key=feature_flag_key, + filters={ + "groups": [ + { + "rollout_percentage": 20, + "properties": [ + { + "key": "id", + "type": "cohort", + "value": head_cohort.pk, # link "head" cohort + } + ], + } + ] + }, + ) + + url = f"/api/organizations/{self.organization.id}/feature_flags/copy_flags" + target_project = self.team_2 + + data = { + "feature_flag_key": flag_to_copy.key, + "from_project": flag_to_copy.team_id, + "target_project_ids": [target_project.id], + } + response = self.client.post(url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # check all cohorts were created in the destination project + for name in cohorts.keys(): + found_cohort = Cohort.objects.filter(name=name, team_id=target_project.id)[0] + self.assertTrue(found_cohort) + + # destination flag contains the head cohort + destination_flag = FeatureFlag.objects.get(key=feature_flag_key, team_id=target_project.id) + destination_flag_head_cohort_id = destination_flag.filters["groups"][0]["properties"][0]["value"] + destination_head_cohort = Cohort.objects.get(pk=destination_flag_head_cohort_id, team_id=target_project.id) + self.assertEqual(destination_head_cohort.name, head_cohort.name) + self.assertNotEqual(destination_head_cohort.id, head_cohort.id) + + # get topological order of the original cohorts + original_cohorts_cache = {} + for _, cohort in cohorts.items(): + original_cohorts_cache[str(cohort.id)] = cohort + original_cohort_ids = {int(str_id) for str_id in original_cohorts_cache.keys()} + topologically_sorted_original_cohort_ids = sort_cohorts_topologically( + original_cohort_ids, original_cohorts_cache + ) + + # drill down the destination cohorts in the reverse topological order + # the order of names should match the reverse topological order of the original cohort names + topologically_sorted_original_cohort_ids_reversed = topologically_sorted_original_cohort_ids[::-1] + + def traverse(cohort, index): + expected_cohort_id = topologically_sorted_original_cohort_ids_reversed[index] + expected_name = original_cohorts_cache[str(expected_cohort_id)].name + self.assertEqual(expected_name, cohort.name) + + prop = cohort.filters["properties"]["values"][0] + if prop["type"] == "cohort": + next_cohort_id = prop["value"] + next_cohort = Cohort.objects.get(pk=next_cohort_id, team_id=target_project.id) + traverse(next_cohort, index + 1) + + traverse(destination_head_cohort, 0) + + def test_copy_feature_flag_destination_cohort_not_overridden(self): + cohort_name = "cohort-1" + target_project = self.team_2 + original_cohort = Cohort.objects.create( + team=self.team, + name=cohort_name, + groups=[{"properties": [{"key": "$some_prop", "value": "original_value", "type": "person"}]}], + ) + + destination_cohort_prop_value = "destination_value" + Cohort.objects.create( + team=target_project, + name=cohort_name, + groups=[{"properties": [{"key": "$some_prop", "value": destination_cohort_prop_value, "type": "person"}]}], + ) + + flag_to_copy = FeatureFlag.objects.create( + team=self.team_1, + created_by=self.user, + key="flag-with-cohort", + filters={ + "groups": [ + { + "rollout_percentage": 20, + "properties": [ + { + "key": "id", + "type": "cohort", + "value": original_cohort.pk, + } + ], + } + ] + }, + ) + + url = f"/api/organizations/{self.organization.id}/feature_flags/copy_flags" + + data = { + "feature_flag_key": flag_to_copy.key, + "from_project": flag_to_copy.team_id, + "target_project_ids": [target_project.id], + } + response = self.client.post(url, data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + destination_cohort = Cohort.objects.filter(name=cohort_name, team=target_project).first() + self.assertTrue(destination_cohort is not None) + # check destination value not overwritten + + if destination_cohort is not None: + self.assertTrue(destination_cohort.groups[0]["properties"][0]["value"] == destination_cohort_prop_value) diff --git a/posthog/api/test/test_survey.py b/posthog/api/test/test_survey.py index 92008ce32657d..75cd3d1c91e5b 100644 --- a/posthog/api/test/test_survey.py +++ b/posthog/api/test/test_survey.py @@ -365,7 +365,7 @@ def test_updating_survey_with_targeting_creates_or_updates_targeting_flag(self): "groups": [{"variant": None, "properties": [], "rollout_percentage": 20}] } - def test_updating_survey_to_remove_targeting_doesnt_delete_targeting_flag(self): + def test_updating_survey_to_send_none_targeting_doesnt_delete_targeting_flag(self): survey_with_targeting = self.client.post( f"/api/projects/{self.team.id}/surveys/", data={ @@ -409,7 +409,7 @@ def test_updating_survey_to_remove_targeting_doesnt_delete_targeting_flag(self): assert FeatureFlag.objects.filter(id=flagId).exists() - def test_updating_survey_to_send_none_targeting_deletes_targeting_flag(self): + def test_updating_survey_to_remove_targeting_deletes_targeting_flag(self): survey_with_targeting = self.client.post( f"/api/projects/{self.team.id}/surveys/", data={ @@ -697,6 +697,58 @@ def test_deleting_survey_deletes_targeting_flag(self): assert deleted_survey.status_code == status.HTTP_204_NO_CONTENT assert not FeatureFlag.objects.filter(id=response.json()["targeting_flag"]["id"]).exists() + def test_inactive_surveys_disables_targeting_flag(self): + survey_with_targeting = self.client.post( + f"/api/projects/{self.team.id}/surveys/", + data={ + "name": "survey with targeting", + "type": "popover", + "targeting_flag_filters": { + "groups": [ + { + "variant": None, + "rollout_percentage": None, + "properties": [ + { + "key": "billing_plan", + "value": ["cloud"], + "operator": "exact", + "type": "person", + } + ], + } + ] + }, + "conditions": {"url": "https://app.posthog.com/notebooks"}, + }, + format="json", + ).json() + assert FeatureFlag.objects.filter(id=survey_with_targeting["targeting_flag"]["id"]).get().active is False + # launch survey + self.client.patch( + f"/api/projects/{self.team.id}/surveys/{survey_with_targeting['id']}/", + data={ + "start_date": datetime.now() - timedelta(days=1), + }, + ) + assert FeatureFlag.objects.filter(id=survey_with_targeting["targeting_flag"]["id"]).get().active is True + # stop the survey + self.client.patch( + f"/api/projects/{self.team.id}/surveys/{survey_with_targeting['id']}/", + data={ + "end_date": datetime.now() + timedelta(days=1), + }, + ) + assert FeatureFlag.objects.filter(id=survey_with_targeting["targeting_flag"]["id"]).get().active is False + # resume survey again + self.client.patch( + f"/api/projects/{self.team.id}/surveys/{survey_with_targeting['id']}/", + data={ + "end_date": None, + }, + ) + assert FeatureFlag.objects.filter(id=survey_with_targeting["targeting_flag"]["id"]).get().active is True + def test_can_list_surveys(self): self.client.post( f"/api/projects/{self.team.id}/surveys/", diff --git a/posthog/celery.py b/posthog/celery.py index a7b62848bfab3..46b1e3b402f71 100644 --- a/posthog/celery.py +++ b/posthog/celery.py @@ -27,7 +27,8 @@ from posthog.cloud_utils import is_cloud from posthog.metrics import pushed_metrics_registry from posthog.redis import get_client -from posthog.utils import get_crontab, get_instance_region +from posthog.utils import get_crontab +from posthog.ph_client import get_ph_client # set the default Django settings module for the 'celery' program. os.environ.setdefault("DJANGO_SETTINGS_MODULE", "posthog.settings") @@ -333,6 +334,13 @@ def setup_periodic_tasks(sender: Celery, **kwargs): name="sync datawarehouse sources that have settled in s3 bucket", ) + # Every 30 minutes try to retrieve and calculate total rows synced in period + sender.add_periodic_task( + crontab(minute="*/30"), + calculate_external_data_rows_synced.s(), + name="calculate external data rows synced", + ) + # Set up clickhouse query instrumentation @task_prerun.connect @@ -903,29 +911,10 @@ def debug_task(self): @app.task(ignore_result=True) def calculate_decide_usage() -> None: from django.db.models import Q - from posthoganalytics import Posthog - from posthog.models import Team from posthog.models.feature_flag.flag_analytics import capture_team_decide_usage - if not is_cloud(): - return - - # send EU data to EU, US data to US - api_key = None - host = None - region = get_instance_region() - if region == "EU": - api_key = "phc_dZ4GK1LRjhB97XozMSkEwPXx7OVANaJEwLErkY1phUF" - host = "https://eu.posthog.com" - elif region == "US": - api_key = "sTMFPsFhdP1Ssg" - host = "https://app.posthog.com" - - if not api_key: - return - - ph_client = Posthog(api_key, host=host) + ph_client = get_ph_client() for team in Team.objects.select_related("organization").exclude( Q(organization__for_internal_metrics=True) | Q(is_demo=True) @@ -935,6 +924,22 @@ def calculate_decide_usage() -> None: ph_client.shutdown() +@app.task(ignore_result=True) +def calculate_external_data_rows_synced() -> None: + from django.db.models import Q + from posthog.models import Team + from posthog.tasks.warehouse import ( + capture_workspace_rows_synced_by_team, + check_external_data_source_billing_limit_by_team, + ) + + for team in Team.objects.select_related("organization").exclude( + Q(organization__for_internal_metrics=True) | Q(is_demo=True) | Q(external_data_workspace_id__isnull=True) + ): + capture_workspace_rows_synced_by_team.delay(team.pk) + check_external_data_source_billing_limit_by_team.delay(team.pk) + + @app.task(ignore_result=True) def find_flags_with_enriched_analytics(): from datetime import datetime, timedelta @@ -1092,7 +1097,7 @@ def ee_persist_finished_recordings(): @app.task(ignore_result=True) def sync_datawarehouse_sources(): try: - from posthog.warehouse.sync_resource import sync_resources + from posthog.tasks.warehouse import sync_resources except ImportError: pass else: diff --git a/posthog/migrations/0364_team_external_data_workspace_rows.py b/posthog/migrations/0364_team_external_data_workspace_rows.py new file mode 100644 index 0000000000000..ec9478becd1c9 --- /dev/null +++ b/posthog/migrations/0364_team_external_data_workspace_rows.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.19 on 2023-11-07 20:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("posthog", "0363_add_replay_payload_capture_config"), + ] + + operations = [ + migrations.AddField( + model_name="team", + name="external_data_workspace_last_synced_at", + field=models.DateTimeField(blank=True, null=True), + ) + ] diff --git a/posthog/models/cohort/util.py b/posthog/models/cohort/util.py index 800b937d51f15..abd4e6c89920c 100644 --- a/posthog/models/cohort/util.py +++ b/posthog/models/cohort/util.py @@ -1,6 +1,6 @@ import uuid from datetime import datetime, timedelta -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Set, Tuple, Union import structlog from dateutil import parser @@ -468,3 +468,53 @@ def get_dependent_cohorts( continue return cohorts + + +def sort_cohorts_topologically(cohort_ids: Set[int], seen_cohorts_cache: Dict[str, Cohort]) -> List[int]: + """ + Sorts the given cohorts in an order where cohorts with no dependencies are placed first, + followed by cohorts that depend on the preceding ones. It ensures that each cohort in the sorted list + only depends on cohorts that appear earlier in the list. + """ + + if not cohort_ids: + return [] + + dependency_graph: Dict[int, List[int]] = {} + seen = set() + + # build graph (adjacency list) + def traverse(cohort): + # add parent + dependency_graph[cohort.id] = [] + for prop in cohort.properties.flat: + if prop.type == "cohort" and not isinstance(prop.value, list): + # add child + dependency_graph[cohort.id].append(int(prop.value)) + + neighbor_cohort = seen_cohorts_cache[str(prop.value)] + if cohort.id not in seen: + seen.add(cohort.id) + traverse(neighbor_cohort) + + for cohort_id in cohort_ids: + cohort = seen_cohorts_cache[str(cohort_id)] + traverse(cohort) + + # post-order DFS (children first, then the parent) + def dfs(node, seen, sorted_arr): + neighbors = dependency_graph.get(node, []) + for neighbor in neighbors: + if neighbor not in seen: + dfs(neighbor, seen, sorted_arr) + sorted_arr.append(int(node)) + seen.add(node) + + sorted_cohort_ids: List[int] = [] + seen = set() + for cohort_id in cohort_ids: + if cohort_id not in seen: + seen.add(cohort_id) + dfs(cohort_id, seen, sorted_cohort_ids) + + return sorted_cohort_ids diff --git a/posthog/models/feature_flag/feature_flag.py b/posthog/models/feature_flag/feature_flag.py index 97d5d3e1aace8..36379563aa7f7 100644 --- a/posthog/models/feature_flag/feature_flag.py +++ b/posthog/models/feature_flag/feature_flag.py @@ -260,8 +260,9 @@ def get_cohort_ids( self, using_database: str = "default", seen_cohorts_cache: Optional[Dict[str, Cohort]] = None, + sort_by_topological_order=False, ) -> List[int]: - from posthog.models.cohort.util import get_dependent_cohorts + from posthog.models.cohort.util import get_dependent_cohorts, sort_cohorts_topologically if seen_cohorts_cache is None: seen_cohorts_cache = {} @@ -293,6 +294,8 @@ def get_cohort_ids( ) except Cohort.DoesNotExist: continue + if sort_by_topological_order: + return sort_cohorts_topologically(cohort_ids, seen_cohorts_cache) return list(cohort_ids) diff --git a/posthog/models/filters/test/__snapshots__/test_filter.ambr b/posthog/models/filters/test/__snapshots__/test_filter.ambr index 648679a965912..c236a5d28a6fa 100644 --- a/posthog/models/filters/test/__snapshots__/test_filter.ambr +++ b/posthog/models/filters/test/__snapshots__/test_filter.ambr @@ -49,7 +49,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -106,7 +107,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -163,7 +165,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -220,7 +223,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -277,7 +281,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 diff --git a/posthog/models/organization.py b/posthog/models/organization.py index 4e1c7af79838c..869ba9f0f6e75 100644 --- a/posthog/models/organization.py +++ b/posthog/models/organization.py @@ -53,6 +53,7 @@ class OrganizationUsageResource(TypedDict): class OrganizationUsageInfo(TypedDict): events: Optional[OrganizationUsageResource] recordings: Optional[OrganizationUsageResource] + rows_synced: Optional[OrganizationUsageResource] period: Optional[List[str]] diff --git a/posthog/models/team/team.py b/posthog/models/team/team.py index 2f5654e0f039a..d03799ad2343b 100644 --- a/posthog/models/team/team.py +++ b/posthog/models/team/team.py @@ -247,6 +247,7 @@ def aggregate_users_by_distinct_id(self) -> bool: event_properties_with_usage: models.JSONField = models.JSONField(default=list, blank=True) event_properties_numerical: models.JSONField = models.JSONField(default=list, blank=True) external_data_workspace_id: models.CharField = models.CharField(max_length=400, null=True, blank=True) + external_data_workspace_last_synced_at: models.DateTimeField = models.DateTimeField(null=True, blank=True) objects: TeamManager = TeamManager() diff --git a/posthog/ph_client.py b/posthog/ph_client.py new file mode 100644 index 0000000000000..e81161a59d470 --- /dev/null +++ b/posthog/ph_client.py @@ -0,0 +1,27 @@ +from posthog.utils import get_instance_region +from posthog.cloud_utils import is_cloud + + +def get_ph_client(): + from posthoganalytics import Posthog + + if not is_cloud(): + return + + # send EU data to EU, US data to US + api_key = None + host = None + region = get_instance_region() + if region == "EU": + api_key = "phc_dZ4GK1LRjhB97XozMSkEwPXx7OVANaJEwLErkY1phUF" + host = "https://eu.posthog.com" + elif region == "US": + api_key = "sTMFPsFhdP1Ssg" + host = "https://app.posthog.com" + + if not api_key: + return + + ph_client = Posthog(api_key, host=host) + + return ph_client diff --git a/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr b/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr index 883b68b6b75f1..0afe5e0ac247d 100644 --- a/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr +++ b/posthog/session_recordings/test/__snapshots__/test_session_recordings.ambr @@ -49,7 +49,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -106,7 +107,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -163,7 +165,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -220,7 +223,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -277,7 +281,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -356,7 +361,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -487,7 +493,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -681,7 +688,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -753,7 +761,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -810,7 +819,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -867,7 +877,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -924,7 +935,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -981,7 +993,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -1038,7 +1051,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -1117,7 +1131,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -1185,7 +1200,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -1264,7 +1280,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -1540,7 +1557,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -1619,7 +1637,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -1908,7 +1927,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -1987,7 +2007,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -2234,7 +2255,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -2321,7 +2343,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -2400,7 +2423,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -2700,7 +2724,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -2779,7 +2804,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -2829,7 +2855,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -3545,7 +3572,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -3624,7 +3652,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -3892,7 +3921,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -3982,7 +4012,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -4252,7 +4283,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -4331,7 +4363,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ @@ -4614,7 +4647,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -4693,7 +4727,8 @@ "posthog_team"."extra_settings", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 /*controller='project_session_recordings-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/session_recordings/%3F%24'*/ diff --git a/posthog/tasks/test/__snapshots__/test_usage_report.ambr b/posthog/tasks/test/__snapshots__/test_usage_report.ambr index 74f71be82a5cc..92f4167485468 100644 --- a/posthog/tasks/test/__snapshots__/test_usage_report.ambr +++ b/posthog/tasks/test/__snapshots__/test_usage_report.ambr @@ -255,6 +255,24 @@ GROUP BY team_id ' --- +# name: TestFeatureFlagsUsageReport.test_usage_report_decide_requests.24 + ' + + SELECT team, + sum(rows_synced) + FROM + (SELECT JSONExtractString(properties, 'job_id') AS job_id, + distinct_id AS team, + any(JSONExtractInt(properties, 'count')) AS rows_synced + FROM events + WHERE team_id = 2 + AND event = 'external data sync job' + AND parseDateTimeBestEffort(JSONExtractString(properties, 'start_time')) BETWEEN '2022-01-10 00:00:00' AND '2022-01-10 23:59:59' + GROUP BY job_id, + team) + GROUP BY team + ' +--- # name: TestFeatureFlagsUsageReport.test_usage_report_decide_requests.3 ' diff --git a/posthog/tasks/test/test_usage_report.py b/posthog/tasks/test/test_usage_report.py index 715c3829855d2..79a7ab46b8ab2 100644 --- a/posthog/tasks/test/test_usage_report.py +++ b/posthog/tasks/test/test_usage_report.py @@ -399,6 +399,7 @@ def _test_usage_report(self) -> List[dict]: "event_explorer_api_bytes_read": 0, "event_explorer_api_rows_read": 0, "event_explorer_api_duration_ms": 0, + "rows_synced_in_period": 0, "date": "2022-01-09", "organization_id": str(self.organization.id), "organization_name": "Test", @@ -440,6 +441,7 @@ def _test_usage_report(self) -> List[dict]: "event_explorer_api_bytes_read": 0, "event_explorer_api_rows_read": 0, "event_explorer_api_duration_ms": 0, + "rows_synced_in_period": 0, }, str(self.org_1_team_2.id): { "event_count_lifetime": 11, @@ -475,6 +477,7 @@ def _test_usage_report(self) -> List[dict]: "event_explorer_api_bytes_read": 0, "event_explorer_api_rows_read": 0, "event_explorer_api_duration_ms": 0, + "rows_synced_in_period": 0, }, }, }, @@ -533,6 +536,7 @@ def _test_usage_report(self) -> List[dict]: "event_explorer_api_bytes_read": 0, "event_explorer_api_rows_read": 0, "event_explorer_api_duration_ms": 0, + "rows_synced_in_period": 0, "date": "2022-01-09", "organization_id": str(self.org_2.id), "organization_name": "Org 2", @@ -574,6 +578,7 @@ def _test_usage_report(self) -> List[dict]: "event_explorer_api_bytes_read": 0, "event_explorer_api_rows_read": 0, "event_explorer_api_duration_ms": 0, + "rows_synced_in_period": 0, } }, }, @@ -980,6 +985,95 @@ def test_usage_report_survey_responses(self, billing_task_mock: MagicMock, posth assert org_2_report["teams"]["5"]["survey_responses_count_in_month"] == 7 +@freeze_time("2022-01-10T00:01:00Z") +class TestExternalDataSyncUsageReport(ClickhouseDestroyTablesMixin, TestCase, ClickhouseTestMixin): + def setUp(self) -> None: + Team.objects.all().delete() + return super().setUp() + + def _setup_teams(self) -> None: + self.analytics_org = Organization.objects.create(name="PostHog") + self.org_1 = Organization.objects.create(name="Org 1") + self.org_2 = Organization.objects.create(name="Org 2") + + self.analytics_team = Team.objects.create(pk=2, organization=self.analytics_org, name="Analytics") + + self.org_1_team_1 = Team.objects.create(pk=3, organization=self.org_1, name="Team 1 org 1") + self.org_1_team_2 = Team.objects.create(pk=4, organization=self.org_1, name="Team 2 org 1") + self.org_2_team_3 = Team.objects.create(pk=5, organization=self.org_2, name="Team 3 org 2") + + @patch("posthog.tasks.usage_report.Client") + @patch("posthog.tasks.usage_report.send_report_to_billing_service") + def test_external_data_rows_synced_response( + self, billing_task_mock: MagicMock, posthog_capture_mock: MagicMock + ) -> None: + self._setup_teams() + + for i in range(5): + start_time = (now() - relativedelta(hours=i)).strftime("%Y-%m-%dT%H:%M:%SZ") + _create_event( + distinct_id="3", + event="external data sync job", + properties={ + "count": 10, + "job_id": 10924, + "start_time": start_time, + }, + timestamp=now() - relativedelta(hours=i), + team=self.analytics_team, + ) + # identical job id should be deduped and not counted + _create_event( + distinct_id="3", + event="external data sync job", + properties={ + "count": 10, + "job_id": 10924, + "start_time": start_time, + }, + timestamp=now() - relativedelta(hours=i, minutes=i), + team=self.analytics_team, + ) + + for i in range(5): + _create_event( + distinct_id="4", + event="external data sync job", + properties={ + "count": 10, + "job_id": 10924, + "start_time": (now() - relativedelta(hours=i)).strftime("%Y-%m-%dT%H:%M:%SZ"), + }, + timestamp=now() - relativedelta(hours=i), + team=self.analytics_team, + ) + + flush_persons_and_events() + + period = get_previous_day(at=now() + relativedelta(days=1)) + period_start, period_end = period + all_reports = _get_all_org_reports(period_start, period_end) + + assert len(all_reports) == 3 + + org_1_report = _get_full_org_usage_report_as_dict( + _get_full_org_usage_report(all_reports[str(self.org_1.id)], get_instance_metadata(period)) + ) + + org_2_report = _get_full_org_usage_report_as_dict( + _get_full_org_usage_report(all_reports[str(self.org_2.id)], get_instance_metadata(period)) + ) + + assert org_1_report["organization_name"] == "Org 1" + assert org_1_report["rows_synced_in_period"] == 20 + + assert org_1_report["teams"]["3"]["rows_synced_in_period"] == 10 + assert org_1_report["teams"]["4"]["rows_synced_in_period"] == 10 + + assert org_2_report["organization_name"] == "Org 2" + assert org_2_report["rows_synced_in_period"] == 0 + + class SendUsageTest(LicensedTestMixin, ClickhouseDestroyTablesMixin, APIBaseTest): def setUp(self) -> None: super().setUp() @@ -1039,6 +1133,10 @@ def _usage_report_response(self) -> Any: "usage": 1000, "limit": None, }, + "rows_synced": { + "usage": 1000, + "limit": None, + }, }, } } @@ -1185,6 +1283,7 @@ def test_org_usage_updated_correctly(self, mock_post: MagicMock, mock_client: Ma assert self.team.organization.usage == { "events": {"limit": None, "usage": 10000, "todays_usage": 0}, "recordings": {"limit": None, "usage": 1000, "todays_usage": 0}, + "rows_synced": {"limit": None, "usage": 1000, "todays_usage": 0}, "period": ["2021-10-01T00:00:00Z", "2021-10-31T00:00:00Z"], } diff --git a/posthog/tasks/test/test_warehouse.py b/posthog/tasks/test/test_warehouse.py new file mode 100644 index 0000000000000..20b669b754995 --- /dev/null +++ b/posthog/tasks/test/test_warehouse.py @@ -0,0 +1,167 @@ +from posthog.test.base import APIBaseTest +import datetime +from unittest.mock import patch, MagicMock +from posthog.tasks.warehouse import ( + _traverse_jobs_by_field, + capture_workspace_rows_synced_by_team, + check_external_data_source_billing_limit_by_team, +) +from posthog.warehouse.models import ExternalDataSource +from freezegun import freeze_time + + +class TestWarehouse(APIBaseTest): + @patch("posthog.tasks.warehouse.send_request") + @freeze_time("2023-11-07") + def test_traverse_jobs_by_field(self, send_request_mock: MagicMock) -> None: + send_request_mock.return_value = { + "data": [ + { + "jobId": 5827835, + "status": "succeeded", + "jobType": "sync", + "startTime": "2023-11-07T16:50:49Z", + "connectionId": "fake", + "lastUpdatedAt": "2023-11-07T16:52:54Z", + "duration": "PT2M5S", + "rowsSynced": 93353, + }, + { + "jobId": 5783573, + "status": "succeeded", + "jobType": "sync", + "startTime": "2023-11-05T18:32:41Z", + "connectionId": "fake-2", + "lastUpdatedAt": "2023-11-05T18:35:11Z", + "duration": "PT2M30S", + "rowsSynced": 97747, + }, + ] + } + mock_capture = MagicMock() + response = _traverse_jobs_by_field(mock_capture, self.team, "fake-url", "rowsSynced") + + self.assertEqual( + response, + [ + {"count": 93353, "startTime": "2023-11-07T16:50:49Z"}, + {"count": 97747, "startTime": "2023-11-05T18:32:41Z"}, + ], + ) + + self.assertEqual(mock_capture.capture.call_count, 2) + mock_capture.capture.assert_called_with( + self.team.pk, + "external data sync job", + { + "count": 97747, + "workspace_id": self.team.external_data_workspace_id, + "team_id": self.team.pk, + "team_uuid": self.team.uuid, + "startTime": "2023-11-05T18:32:41Z", + "job_id": "5783573", + }, + ) + + @patch("posthog.tasks.warehouse._traverse_jobs_by_field") + @patch("posthog.tasks.warehouse.get_ph_client") + @freeze_time("2023-11-07") + def test_capture_workspace_rows_synced_by_team( + self, mock_capture: MagicMock, traverse_jobs_mock: MagicMock + ) -> None: + traverse_jobs_mock.return_value = [ + {"count": 97747, "startTime": "2023-11-05T18:32:41Z"}, + {"count": 93353, "startTime": "2023-11-07T16:50:49Z"}, + ] + + capture_workspace_rows_synced_by_team(self.team.pk) + + self.team.refresh_from_db() + self.assertEqual( + self.team.external_data_workspace_last_synced_at, + datetime.datetime(2023, 11, 7, 16, 50, 49, tzinfo=datetime.timezone.utc), + ) + + @patch("posthog.tasks.warehouse._traverse_jobs_by_field") + @patch("posthog.tasks.warehouse.get_ph_client") + @freeze_time("2023-11-07") + def test_capture_workspace_rows_synced_by_team_month_cutoff( + self, mock_capture: MagicMock, traverse_jobs_mock: MagicMock + ) -> None: + # external_data_workspace_last_synced_at unset + traverse_jobs_mock.return_value = [ + {"count": 93353, "startTime": "2023-11-07T16:50:49Z"}, + ] + + capture_workspace_rows_synced_by_team(self.team.pk) + + self.team.refresh_from_db() + self.assertEqual( + self.team.external_data_workspace_last_synced_at, + datetime.datetime(2023, 11, 7, 16, 50, 49, tzinfo=datetime.timezone.utc), + ) + + @patch("posthog.tasks.warehouse._traverse_jobs_by_field") + @patch("posthog.tasks.warehouse.get_ph_client") + @freeze_time("2023-11-07") + def test_capture_workspace_rows_synced_by_team_month_cutoff_field_set( + self, mock_capture: MagicMock, traverse_jobs_mock: MagicMock + ) -> None: + self.team.external_data_workspace_last_synced_at = datetime.datetime( + 2023, 10, 29, 18, 32, 41, tzinfo=datetime.timezone.utc + ) + self.team.save() + traverse_jobs_mock.return_value = [ + {"count": 97747, "startTime": "2023-10-30T18:32:41Z"}, + {"count": 93353, "startTime": "2023-11-07T16:50:49Z"}, + ] + + capture_workspace_rows_synced_by_team(self.team.pk) + + self.team.refresh_from_db() + self.assertEqual( + self.team.external_data_workspace_last_synced_at, + datetime.datetime(2023, 11, 7, 16, 50, 49, tzinfo=datetime.timezone.utc), + ) + + @patch("posthog.warehouse.external_data_source.connection.send_request") + @patch("ee.billing.quota_limiting.list_limited_team_attributes") + def test_external_data_source_billing_limit_deactivate( + self, usage_limit_mock: MagicMock, send_request_mock: MagicMock + ) -> None: + usage_limit_mock.return_value = [self.team.pk] + + external_source = ExternalDataSource.objects.create( + source_id="test_id", + connection_id="fake connectino_id", + destination_id="fake destination_id", + team=self.team, + status="running", + source_type="Stripe", + ) + + check_external_data_source_billing_limit_by_team(self.team.pk) + + external_source.refresh_from_db() + self.assertEqual(external_source.status, "inactive") + + @patch("posthog.warehouse.external_data_source.connection.send_request") + @patch("ee.billing.quota_limiting.list_limited_team_attributes") + def test_external_data_source_billing_limit_activate( + self, usage_limit_mock: MagicMock, send_request_mock: MagicMock + ) -> None: + usage_limit_mock.return_value = [] + + external_source = ExternalDataSource.objects.create( + source_id="test_id", + connection_id="fake connectino_id", + destination_id="fake destination_id", + team=self.team, + status="inactive", + source_type="Stripe", + ) + + check_external_data_source_billing_limit_by_team(self.team.pk) + + external_source.refresh_from_db() + self.assertEqual(external_source.status, "running") diff --git a/posthog/tasks/usage_report.py b/posthog/tasks/usage_report.py index a9a06ecbff7c5..9ffbc39227331 100644 --- a/posthog/tasks/usage_report.py +++ b/posthog/tasks/usage_report.py @@ -110,6 +110,8 @@ class UsageReportCounters: # Surveys survey_responses_count_in_period: int survey_responses_count_in_month: int + # Data Warehouse + rows_synced_in_period: int # Instance metadata to be included in oveall report @@ -591,6 +593,34 @@ def get_teams_with_survey_responses_count_in_period( return results +@timed_log() +@retry(tries=QUERY_RETRIES, delay=QUERY_RETRY_DELAY, backoff=QUERY_RETRY_BACKOFF) +def get_teams_with_rows_synced_in_period(begin: datetime, end: datetime) -> List[Tuple[int, int]]: + team_to_query = 1 if get_instance_region() == "EU" else 2 + + # dedup by job id incase there were duplicates sent + results = sync_execute( + """ + SELECT team, sum(rows_synced) FROM ( + SELECT JSONExtractString(properties, 'job_id') AS job_id, distinct_id AS team, any(JSONExtractInt(properties, 'count')) AS rows_synced + FROM events + WHERE team_id = %(team_to_query)s AND event = 'external data sync job' AND parseDateTimeBestEffort(JSONExtractString(properties, 'start_time')) BETWEEN %(begin)s AND %(end)s + GROUP BY job_id, team + ) + GROUP BY team + """, + { + "begin": begin, + "end": end, + "team_to_query": team_to_query, + }, + workload=Workload.OFFLINE, + settings=CH_BILLING_SETTINGS, + ) + + return results + + @app.task(ignore_result=True, max_retries=0) def capture_report( capture_event_name: str, @@ -784,6 +814,7 @@ def _get_all_usage_data(period_start: datetime, period_end: datetime) -> Dict[st teams_with_survey_responses_count_in_month=get_teams_with_survey_responses_count_in_period( period_start.replace(day=1), period_end ), + teams_with_rows_synced_in_period=get_teams_with_rows_synced_in_period(period_start, period_end), ) @@ -854,6 +885,7 @@ def _get_team_report(all_data: Dict[str, Any], team: Team) -> UsageReportCounter event_explorer_api_duration_ms=all_data["teams_with_event_explorer_api_duration_ms"].get(team.id, 0), survey_responses_count_in_period=all_data["teams_with_survey_responses_count_in_period"].get(team.id, 0), survey_responses_count_in_month=all_data["teams_with_survey_responses_count_in_month"].get(team.id, 0), + rows_synced_in_period=all_data["teams_with_rows_synced_in_period"].get(team.id, 0), ) diff --git a/posthog/tasks/warehouse.py b/posthog/tasks/warehouse.py new file mode 100644 index 0000000000000..2450251830c59 --- /dev/null +++ b/posthog/tasks/warehouse.py @@ -0,0 +1,167 @@ +from django.conf import settings +import datetime +from posthog.models import Team +from posthog.warehouse.external_data_source.client import send_request +from posthog.warehouse.models.external_data_source import ExternalDataSource +from posthog.warehouse.models import DataWarehouseCredential, DataWarehouseTable +from posthog.warehouse.external_data_source.connection import retrieve_sync +from urllib.parse import urlencode +from posthog.ph_client import get_ph_client +from typing import Any, Dict, List, TYPE_CHECKING +from posthog.celery import app +import structlog + +logger = structlog.get_logger(__name__) + +AIRBYTE_JOBS_URL = "https://api.airbyte.com/v1/jobs" +DEFAULT_DATE_TIME = datetime.datetime(2023, 11, 7, tzinfo=datetime.timezone.utc) + +if TYPE_CHECKING: + from posthoganalytics import Posthog + + +def sync_resources() -> None: + resources = ExternalDataSource.objects.filter(are_tables_created=False, status__in=["running", "error"]) + + for resource in resources: + sync_resource.delay(resource.pk) + + +@app.task(ignore_result=True) +def sync_resource(resource_id: str) -> None: + resource = ExternalDataSource.objects.get(pk=resource_id) + + try: + job = retrieve_sync(resource.connection_id) + except Exception as e: + logger.exception("Data Warehouse: Sync Resource failed with an unexpected exception.", exc_info=e) + resource.status = "error" + resource.save() + return + + if job is None: + logger.error(f"Data Warehouse: No jobs found for connection: {resource.connection_id}") + resource.status = "error" + resource.save() + return + + if job["status"] == "succeeded": + resource = ExternalDataSource.objects.get(pk=resource_id) + credential, _ = DataWarehouseCredential.objects.get_or_create( + team_id=resource.team.pk, + access_key=settings.AIRBYTE_BUCKET_KEY, + access_secret=settings.AIRBYTE_BUCKET_SECRET, + ) + + data = { + "credential": credential, + "name": "stripe_customers", + "format": "Parquet", + "url_pattern": f"https://{settings.AIRBYTE_BUCKET_DOMAIN}/airbyte/{resource.team.pk}/customers/*.parquet", + "team_id": resource.team.pk, + } + + table = DataWarehouseTable(**data) + try: + table.columns = table.get_columns() + except Exception as e: + logger.exception( + f"Data Warehouse: Sync Resource failed with an unexpected exception for connection: {resource.connection_id}", + exc_info=e, + ) + else: + table.save() + + resource.are_tables_created = True + resource.status = job["status"] + resource.save() + + else: + resource.status = job["status"] + resource.save() + + +DEFAULT_USAGE_LIMIT = 1000000 +ROWS_PER_DOLLAR = 66666 # 1 million rows per $15 + + +@app.task(ignore_result=True, max_retries=2) +def check_external_data_source_billing_limit_by_team(team_id: int) -> None: + from posthog.warehouse.external_data_source.connection import deactivate_connection_by_id, activate_connection_by_id + from ee.billing.quota_limiting import list_limited_team_attributes, QuotaResource + + limited_teams_rows_synced = list_limited_team_attributes(QuotaResource.ROWS_SYNCED) + + team = Team.objects.get(pk=team_id) + all_active_connections = ExternalDataSource.objects.filter(team=team, status__in=["running", "succeeded"]) + all_inactive_connections = ExternalDataSource.objects.filter(team=team, status="inactive") + + # TODO: consider more boundaries + if team_id in limited_teams_rows_synced: + for connection in all_active_connections: + deactivate_connection_by_id(connection.connection_id) + connection.status = "inactive" + connection.save() + else: + for connection in all_inactive_connections: + activate_connection_by_id(connection.connection_id) + connection.status = "running" + connection.save() + + +@app.task(ignore_result=True, max_retries=2) +def capture_workspace_rows_synced_by_team(team_id: int) -> None: + ph_client = get_ph_client() + team = Team.objects.get(pk=team_id) + now = datetime.datetime.now(datetime.timezone.utc) + begin = team.external_data_workspace_last_synced_at or DEFAULT_DATE_TIME + + params = { + "workspaceIds": team.external_data_workspace_id, + "limit": 100, + "offset": 0, + "status": "succeeded", + "orderBy": "createdAt|ASC", + "updatedAtStart": begin.strftime("%Y-%m-%dT%H:%M:%SZ"), + "updatedAtEnd": now.strftime("%Y-%m-%dT%H:%M:%SZ"), + } + result_totals = _traverse_jobs_by_field(ph_client, team, AIRBYTE_JOBS_URL + "?" + urlencode(params), "rowsSynced") + + # TODO: check assumption that ordering is possible with API + team.external_data_workspace_last_synced_at = result_totals[-1]["startTime"] if result_totals else now + team.save() + + ph_client.shutdown() + + +def _traverse_jobs_by_field( + ph_client: "Posthog", team: Team, url: str, field: str, acc: List[Dict[str, Any]] = [] +) -> List[Dict[str, Any]]: + response = send_request(url, method="GET") + response_data = response.get("data", []) + response_next = response.get("next", None) + + for job in response_data: + acc.append( + { + "count": job[field], + "startTime": job["startTime"], + } + ) + ph_client.capture( + team.pk, + "external data sync job", + { + "count": job[field], + "workspace_id": team.external_data_workspace_id, + "team_id": team.pk, + "team_uuid": team.uuid, + "startTime": job["startTime"], + "job_id": str(job["jobId"]), + }, + ) + + if response_next: + return _traverse_jobs_by_field(ph_client, team, response_next, field, acc) + + return acc diff --git a/posthog/temporal/tests/batch_exports/test_redshift_batch_export_workflow.py b/posthog/temporal/tests/batch_exports/test_redshift_batch_export_workflow.py index 6a0448b24cfd5..cea71a458013f 100644 --- a/posthog/temporal/tests/batch_exports/test_redshift_batch_export_workflow.py +++ b/posthog/temporal/tests/batch_exports/test_redshift_batch_export_workflow.py @@ -66,13 +66,12 @@ async def assert_events_in_redshift(connection, schema, table_name, events, excl raw_properties = event.get("properties", None) properties = remove_escaped_whitespace_recursive(raw_properties) if raw_properties else None - elements_chain = event.get("elements_chain", None) expected_event = { "distinct_id": event.get("distinct_id"), - "elements": json.dumps(elements_chain) if elements_chain else None, + "elements": "", "event": event_name, "ip": properties.get("$ip", None) if properties else None, - "properties": json.dumps(properties) if properties else None, + "properties": json.dumps(properties, ensure_ascii=False) if properties else None, "set": properties.get("$set", None) if properties else None, "set_once": properties.get("$set_once", None) if properties else None, # Kept for backwards compatibility, but not exported anymore. @@ -185,9 +184,10 @@ async def test_insert_into_redshift_activity_inserts_data_into_redshift_table( properties={ "$browser": "Chrome", "$os": "Mac OS X", - "newline": "\n\n", - "nested_newline": {"newline": "\n\n"}, - "sequence": {"mucho_whitespace": ["\n\n", "\t\t", "\f\f"]}, + "whitespace": "hi\t\n\r\f\bhi", + "nested_whitespace": {"whitespace": "hi\t\n\r\f\bhi"}, + "sequence": {"mucho_whitespace": ["hi", "hi\t\n\r\f\bhi", "hi\t\n\r\f\bhi", "hi"]}, + "multi-byte": "é", }, person_properties={"utm_medium": "referral", "$initial_os": "Linux"}, ) diff --git a/posthog/temporal/workflows/redshift_batch_export.py b/posthog/temporal/workflows/redshift_batch_export.py index 4f107b571a5d0..fd2ba4c9e9193 100644 --- a/posthog/temporal/workflows/redshift_batch_export.py +++ b/posthog/temporal/workflows/redshift_batch_export.py @@ -129,27 +129,28 @@ async def insert_records_to_redshift( rows_exported = get_rows_exported_metric() async with async_client_cursor_from_connection(redshift_connection) as cursor: - batch = [pre_query.as_string(cursor).encode("utf-8")] + batch = [] + pre_query_str = pre_query.as_string(cursor).encode("utf-8") async def flush_to_redshift(batch): - await cursor.execute(b"".join(batch)) - rows_exported.add(len(batch) - 1) + values = b",".join(batch).replace(b" E'", b" '") + + await cursor.execute(pre_query_str + values) + rows_exported.add(len(batch)) # It would be nice to record BYTES_EXPORTED for Redshift, but it's not worth estimating # the byte size of each batch the way things are currently written. We can revisit this # in the future if we decide it's useful enough. for record in itertools.chain([first_record], records): batch.append(cursor.mogrify(template, record).encode("utf-8")) - if len(batch) < batch_size: - batch.append(b",") continue await flush_to_redshift(batch) - batch = [pre_query.as_string(cursor).encode("utf-8")] + batch = [] if len(batch) > 0: - await flush_to_redshift(batch[:-1]) + await flush_to_redshift(batch) @contextlib.asynccontextmanager @@ -278,12 +279,14 @@ async def insert_into_redshift_activity(inputs: RedshiftInsertInputs): def map_to_record(row: dict) -> dict: """Map row to a record to insert to Redshift.""" - return { - key: json.dumps(remove_escaped_whitespace_recursive(row[key])) + record = { + key: json.dumps(remove_escaped_whitespace_recursive(row[key]), ensure_ascii=False) if key in json_columns and row[key] is not None else row[key] for key in schema_columns } + record["elements"] = "" + return record async with postgres_connection(inputs) as connection: await insert_records_to_redshift( diff --git a/posthog/test/__snapshots__/test_feature_flag.ambr b/posthog/test/__snapshots__/test_feature_flag.ambr index d714f9a077c0d..a34d2ba545ae0 100644 --- a/posthog/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/test/__snapshots__/test_feature_flag.ambr @@ -140,7 +140,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 @@ -525,7 +526,8 @@ "posthog_team"."event_properties", "posthog_team"."event_properties_with_usage", "posthog_team"."event_properties_numerical", - "posthog_team"."external_data_workspace_id" + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" WHERE "posthog_team"."id" = 2 LIMIT 21 diff --git a/posthog/warehouse/api/external_data_source.py b/posthog/warehouse/api/external_data_source.py index 3bfcc64e497d1..7510ec26cd02b 100644 --- a/posthog/warehouse/api/external_data_source.py +++ b/posthog/warehouse/api/external_data_source.py @@ -10,7 +10,7 @@ from posthog.warehouse.external_data_source.source import StripeSourcePayload, create_stripe_source, delete_source from posthog.warehouse.external_data_source.connection import create_connection, start_sync from posthog.warehouse.external_data_source.destination import create_destination, delete_destination -from posthog.warehouse.sync_resource import sync_resource +from posthog.tasks.warehouse import sync_resource from posthog.api.routing import StructuredViewSetMixin from rest_framework.decorators import action diff --git a/posthog/warehouse/external_data_source/connection.py b/posthog/warehouse/external_data_source/connection.py index fc89f22abb65b..9a37222f9d8d4 100644 --- a/posthog/warehouse/external_data_source/connection.py +++ b/posthog/warehouse/external_data_source/connection.py @@ -37,6 +37,22 @@ def create_connection(source_id: str, destination_id: str) -> ExternalDataConnec ) +def activate_connection_by_id(connection_id: str): + update_connection_status_by_id(connection_id, "active") + + +def deactivate_connection_by_id(connection_id: str): + update_connection_status_by_id(connection_id, "inactive") + + +def update_connection_status_by_id(connection_id: str, status: str): + connection_id_url = f"{AIRBYTE_CONNECTION_URL}/{connection_id}" + + payload = {"status": status} + + send_request(connection_id_url, method="PATCH", payload=payload) + + def update_connection_stream(connection_id: str): connection_id_url = f"{AIRBYTE_CONNECTION_URL}/{connection_id}" diff --git a/posthog/warehouse/external_data_source/workspace.py b/posthog/warehouse/external_data_source/workspace.py index e92c07fc888cd..ceb8ed50ac33f 100644 --- a/posthog/warehouse/external_data_source/workspace.py +++ b/posthog/warehouse/external_data_source/workspace.py @@ -1,6 +1,7 @@ from posthog.models import Team from posthog.warehouse.external_data_source.client import send_request from django.conf import settings +import datetime AIRBYTE_WORKSPACE_URL = "https://api.airbyte.com/v1/workspaces" @@ -23,6 +24,8 @@ def get_or_create_workspace(team_id: int): if not team.external_data_workspace_id: workspace_id = create_workspace(team_id) team.external_data_workspace_id = workspace_id + # start tracking from now + team.external_data_workspace_last_synced_at = datetime.datetime.now(datetime.timezone.utc) team.save() return team.external_data_workspace_id diff --git a/posthog/warehouse/sync_resource.py b/posthog/warehouse/sync_resource.py deleted file mode 100644 index 3072bf43986d9..0000000000000 --- a/posthog/warehouse/sync_resource.py +++ /dev/null @@ -1,69 +0,0 @@ -from posthog.warehouse.models.external_data_source import ExternalDataSource -from posthog.warehouse.models import DataWarehouseCredential, DataWarehouseTable -from posthog.warehouse.external_data_source.connection import retrieve_sync -from posthog.celery import app - -from django.conf import settings -import structlog - -logger = structlog.get_logger(__name__) - - -def sync_resources(): - resources = ExternalDataSource.objects.filter(are_tables_created=False, status__in=["running", "error"]) - - for resource in resources: - sync_resource.delay(resource.pk) - - -@app.task(ignore_result=True) -def sync_resource(resource_id): - resource = ExternalDataSource.objects.get(pk=resource_id) - - try: - job = retrieve_sync(resource.connection_id) - except Exception as e: - logger.exception("Data Warehouse: Sync Resource failed with an unexpected exception.", exc_info=e) - resource.status = "error" - resource.save() - return - - if job is None: - logger.error(f"Data Warehouse: No jobs found for connection: {resource.connection_id}") - resource.status = "error" - resource.save() - return - - if job["status"] == "succeeded": - resource = ExternalDataSource.objects.get(pk=resource_id) - credential, _ = DataWarehouseCredential.objects.get_or_create( - team_id=resource.team.pk, - access_key=settings.AIRBYTE_BUCKET_KEY, - access_secret=settings.AIRBYTE_BUCKET_SECRET, - ) - - data = { - "credential": credential, - "name": "stripe_customers", - "format": "Parquet", - "url_pattern": f"https://{settings.AIRBYTE_BUCKET_DOMAIN}/airbyte/{resource.team.pk}/customers/*.parquet", - "team_id": resource.team.pk, - } - - table = DataWarehouseTable(**data) - try: - table.columns = table.get_columns() - except Exception as e: - logger.exception( - f"Data Warehouse: Sync Resource failed with an unexpected exception for connection: {resource.connection_id}", - exc_info=e, - ) - else: - table.save() - - resource.are_tables_created = True - resource.status = job["status"] - resource.save() - else: - resource.status = job["status"] - resource.save() diff --git a/tsconfig.json b/tsconfig.json index 658bedd03e802..1c789a099ec9e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,8 +33,8 @@ "suppressImplicitAnyIndexErrors": true, // Index objects by number "lib": ["dom", "es2019"] }, - "include": ["frontend/**/*", ".storybook/**/*"], - "exclude": ["node_modules/**/*", "staticfiles/**/*", "frontend/dist/**/*", "plugin-server/**/*"], + "include": ["frontend/**/*", "cypress/**/*", ".storybook/**/*"], + "exclude": ["frontend/dist/**/*"], "ts-node": { "compilerOptions": { "module": "commonjs"