Skip to content

Commit cf8f3dd

Browse files
authored
Merge branch 'main' into support-cockroachdb
2 parents bc4539c + 126ed64 commit cf8f3dd

File tree

99 files changed

+3630
-1927
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+3630
-1927
lines changed

.github/workflows/platform-docker-build-test-publish.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -237,28 +237,28 @@ jobs:
237237
- name: Set up release tag variables
238238
id: version-trim
239239
run: |
240-
TAG=${{github.ref_name}}
240+
TAG=${{ github.event.release.tag_name }}
241241
echo "version=${TAG#v}" >> $GITHUB_OUTPUT
242242
243-
- name: Run YAML to Github Output Action
244-
id: yaml-output
245-
uses: christian-ci/action-yaml-github-output@v2
243+
- uses: actions-tools/yaml-outputs@v2
244+
id: chart-yaml
246245
with:
247-
file_path: './chart/charts/flagsmith/Chart.yaml'
246+
file-path: ./chart/charts/flagsmith/Chart.yaml
248247

249-
- name: Update flagsmith-charts values.yaml with latest docker version
248+
- name: Open a PR bumping Flagsmith to ${{ github.event.release.tag_name }}
250249
uses: fjogeleit/yaml-update-action@main
251250
with:
252251
token: ${{ secrets.FLAGSMITH_CHARTS_GITHUB_TOKEN }}
253252
repository: flagsmith/flagsmith-charts
254253
workDir: chart
255254
masterBranchName: 'main'
256255
targetBranch: 'main'
257-
branch: docker-image-version-bump-${{ steps.version-trim.outputs.version }}
256+
branch: deps/bump-flagsmith-${{ steps.version-trim.outputs.version }}
258257
commitChange: true
259258
createPR: true
260-
message: 'Flagsmith docker image version bump'
261-
description: 'Automated PR generated by a release event in https://github.com/Flagsmith/flagsmith'
259+
message: 'deps: bump Flagsmith from ${{ steps.chart-yaml.outputs.appVersion }} to ${{ steps.version-trim.outputs.version }}'
260+
title: '{{message}}'
261+
description: 'Automated PR generated by Flagsmith release [${{ github.event.release.tag_name }}](${{ github.event.release.url }}).'
262262
valueFile: 'charts/flagsmith/Chart.yaml'
263263
changes: |
264264
{

api/app/settings/common.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"django.contrib.contenttypes",
8080
"django.contrib.sessions",
8181
"django.contrib.messages",
82+
"django.contrib.postgres",
8283
"django.contrib.staticfiles",
8384
"django.contrib.humanize",
8485
"rest_framework",
@@ -1025,7 +1026,6 @@
10251026
GOOGLE_ANALYTICS_API_KEY = env("GOOGLE_ANALYTICS_API_KEY", default=None)
10261027
HEADWAY_API_KEY = env("HEADWAY_API_KEY", default=None)
10271028
CRISP_CHAT_API_KEY = env("CRISP_CHAT_API_KEY", default=None)
1028-
MIXPANEL_API_KEY = env("MIXPANEL_API_KEY", default=None)
10291029
SENTRY_API_KEY = env("SENTRY_API_KEY", default=None)
10301030
AMPLITUDE_API_KEY = env("AMPLITUDE_API_KEY", default=None)
10311031
REO_API_KEY = env("REO_API_KEY", default=None)
@@ -1238,7 +1238,8 @@
12381238

12391239
# Define the cooldown duration, in seconds, for password reset emails
12401240
PASSWORD_RESET_EMAIL_COOLDOWN = env.int("PASSWORD_RESET_EMAIL_COOLDOWN", 60 * 60 * 24)
1241-
1241+
# Define the threshold, in minutes, for updating the last login timestamp
1242+
LAST_LOGIN_UPDATE_THRESHOLD_MINUTES = env.int("LAST_LOGIN_UPDATE_THRESHOLD_MINUTES", 30)
12421243
# Limit the count of password reset emails that can be dispatched within the `PASSWORD_RESET_EMAIL_COOLDOWN` timeframe.
12431244
MAX_PASSWORD_RESET_EMAILS = env.int("MAX_PASSWORD_RESET_EMAILS", 5)
12441245

api/app/views.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ def project_overrides(request: Request) -> HttpResponse:
4646
"hideInviteLinks": "DISABLE_INVITE_LINKS",
4747
"linkedinPartnerTracking": "LINKEDIN_PARTNER_TRACKING",
4848
"maintenance": "MAINTENANCE_MODE",
49-
"mixpanel": "MIXPANEL_API_KEY",
5049
"preventEmailPassword": "PREVENT_EMAIL_PASSWORD",
5150
"preventSignup": "PREVENT_SIGNUP",
5251
"reo": "REO_API_KEY",
Lines changed: 130 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from datetime import date, datetime, timedelta
2-
from typing import TYPE_CHECKING, List
32

3+
import structlog
44
from dateutil.relativedelta import relativedelta
55
from django.conf import settings
6-
from django.db.models import Sum
6+
from django.db.models import Q, Sum
77
from django.utils import timezone
88
from rest_framework.exceptions import NotFound
99

10+
from app_analytics import constants
1011
from app_analytics.dataclasses import FeatureEvaluationData, UsageData
1112
from app_analytics.influxdb_wrapper import (
1213
get_events_for_organisation,
@@ -17,86 +18,60 @@
1718
from app_analytics.influxdb_wrapper import (
1819
get_usage_data as get_usage_data_from_influxdb,
1920
)
21+
from app_analytics.mappers import map_annotated_api_usage_buckets_to_usage_data
2022
from app_analytics.models import (
2123
APIUsageBucket,
2224
FeatureEvaluationBucket,
23-
Resource,
2425
)
26+
from app_analytics.types import Labels, PeriodType
2527
from environments.models import Environment
2628
from features.models import Feature
27-
from organisations.models import Organisation
29+
from organisations.models import Organisation, OrganisationSubscriptionInformationCache
2830

29-
from . import constants
30-
from .types import PERIOD_TYPE
31+
logger = structlog.get_logger("app_analytics")
3132

3233

3334
def get_usage_data(
3435
organisation: Organisation,
3536
environment_id: int | None = None,
3637
project_id: int | None = None,
37-
period: PERIOD_TYPE | None = None,
38+
period: PeriodType | None = None,
39+
labels_filter: Labels | None = None,
3840
) -> list[UsageData]:
39-
now = timezone.now()
40-
date_start = date_stop = None
41-
sub_cache = getattr(organisation, "subscription_information_cache", None)
41+
sub_cache = OrganisationSubscriptionInformationCache.objects.filter(
42+
organisation=organisation
43+
).first()
4244

43-
is_subscription_valid = (
44-
sub_cache is not None and sub_cache.is_billing_terms_dates_set()
45+
date_start, date_stop = _get_start_date_and_stop_date_for_subscribed_organisation(
46+
sub_cache=sub_cache,
47+
period=period,
4548
)
4649

47-
if period in (constants.CURRENT_BILLING_PERIOD, constants.PREVIOUS_BILLING_PERIOD):
48-
if not is_subscription_valid:
49-
raise NotFound("No billing periods found for this organisation.")
50-
51-
if TYPE_CHECKING:
52-
assert sub_cache is not None
53-
54-
match period:
55-
case constants.CURRENT_BILLING_PERIOD:
56-
starts_at = sub_cache.current_billing_term_starts_at or now - timedelta(
57-
days=30
58-
)
59-
month_delta = relativedelta(now, starts_at).months
60-
date_start = relativedelta(months=month_delta) + starts_at
61-
date_stop = now
62-
63-
case constants.PREVIOUS_BILLING_PERIOD:
64-
starts_at = sub_cache.current_billing_term_starts_at or now - timedelta(
65-
days=30
66-
)
67-
month_delta = relativedelta(now, starts_at).months - 1
68-
month_delta += relativedelta(now, starts_at).years * 12
69-
date_start = relativedelta(months=month_delta) + starts_at
70-
date_stop = relativedelta(months=month_delta + 1) + starts_at
71-
case constants.NINETY_DAY_PERIOD:
72-
date_start = now - relativedelta(days=90)
73-
date_stop = now
74-
7550
if settings.USE_POSTGRES_FOR_ANALYTICS:
76-
kwargs = {
77-
"organisation": organisation,
78-
"environment_id": environment_id,
79-
"project_id": project_id,
80-
}
81-
if date_start:
82-
assert date_stop
83-
kwargs["date_start"] = date_start # type: ignore[assignment]
84-
kwargs["date_stop"] = date_stop # type: ignore[assignment]
85-
86-
return get_usage_data_from_local_db(**kwargs) # type: ignore[arg-type]
87-
88-
kwargs = {
89-
"organisation_id": organisation.id,
90-
"environment_id": environment_id,
91-
"project_id": project_id,
92-
}
51+
return get_usage_data_from_local_db(
52+
organisation=organisation,
53+
environment_id=environment_id,
54+
project_id=project_id,
55+
date_start=date_start,
56+
date_stop=date_stop,
57+
labels_filter=labels_filter,
58+
)
9359

94-
if date_start:
95-
assert date_stop
96-
kwargs["date_start"] = date_start # type: ignore[assignment]
97-
kwargs["date_stop"] = date_stop # type: ignore[assignment]
60+
if settings.INFLUXDB_TOKEN:
61+
return get_usage_data_from_influxdb(
62+
organisation_id=organisation.id,
63+
environment_id=environment_id,
64+
project_id=project_id,
65+
date_start=date_start,
66+
date_stop=date_stop,
67+
labels_filter=labels_filter,
68+
)
9869

99-
return get_usage_data_from_influxdb(**kwargs) # type: ignore[arg-type]
70+
logger.warning(
71+
"no-analytics-database-configured",
72+
details=constants.NO_ANALYTICS_DATABASE_CONFIGURED_WARNING,
73+
)
74+
return []
10075

10176

10277
def get_usage_data_from_local_db(
@@ -105,7 +80,8 @@ def get_usage_data_from_local_db(
10580
project_id: int | None = None,
10681
date_start: datetime | None = None,
10782
date_stop: datetime | None = None,
108-
) -> List[UsageData]:
83+
labels_filter: Labels | None = None,
84+
) -> list[UsageData]:
10985
if date_start is None:
11086
date_start = timezone.now() - timedelta(days=30)
11187
if date_stop is None:
@@ -127,28 +103,20 @@ def get_usage_data_from_local_db(
127103
if environment_id:
128104
qs = qs.filter(environment_id=environment_id)
129105

106+
if labels_filter:
107+
qs = qs.filter(labels__contains=labels_filter)
108+
130109
qs = (
131110
qs.filter( # type: ignore[assignment]
132111
created_at__date__lte=date_stop,
133112
created_at__date__gt=date_start,
134113
)
135114
.order_by("created_at__date")
136-
.values("created_at__date", "resource")
115+
.values("created_at__date", "resource", "labels")
137116
.annotate(count=Sum("total_count"))
138117
)
139-
data_by_day = {}
140-
for row in qs: # TODO Write proper mappers for this?
141-
day = row["created_at__date"]
142-
if day not in data_by_day:
143-
data_by_day[day] = UsageData(day=day)
144-
if column_name := Resource(row["resource"]).column_name:
145-
setattr(
146-
data_by_day[day],
147-
column_name,
148-
row["count"],
149-
)
150118

151-
return data_by_day.values() # type: ignore[return-value]
119+
return map_annotated_api_usage_buckets_to_usage_data(qs)
152120

153121

154122
def get_total_events_count(organisation) -> int: # type: ignore[no-untyped-def]
@@ -168,30 +136,53 @@ def get_total_events_count(organisation) -> int: # type: ignore[no-untyped-def]
168136

169137

170138
def get_feature_evaluation_data(
171-
feature: Feature, environment_id: int, period: int = 30
172-
) -> List[FeatureEvaluationData]:
139+
feature: Feature,
140+
environment_id: int,
141+
period_days: int = 30,
142+
labels_filter: Labels | None = None,
143+
) -> list[FeatureEvaluationData]:
173144
if settings.USE_POSTGRES_FOR_ANALYTICS:
174145
return get_feature_evaluation_data_from_local_db(
175-
feature, environment_id, period
146+
feature=feature,
147+
environment_id=environment_id,
148+
period_days=period_days,
149+
labels_filter=labels_filter,
176150
)
177-
return get_feature_evaluation_data_from_influxdb(
178-
feature_name=feature.name, environment_id=environment_id, period=f"{period}d"
151+
152+
if settings.INFLUXDB_TOKEN:
153+
return get_feature_evaluation_data_from_influxdb(
154+
feature_name=feature.name,
155+
environment_id=environment_id,
156+
period_days=period_days,
157+
labels_filter=labels_filter,
158+
)
159+
160+
logger.warning(
161+
"no-analytics-database-configured",
162+
details=constants.NO_ANALYTICS_DATABASE_CONFIGURED_WARNING,
179163
)
164+
return []
180165

181166

182167
def get_feature_evaluation_data_from_local_db(
183-
feature: Feature, environment_id: int, period: int = 30
184-
) -> List[FeatureEvaluationData]:
168+
feature: Feature,
169+
environment_id: int,
170+
period_days: int = 30,
171+
labels_filter: Labels | None = None,
172+
) -> list[FeatureEvaluationData]:
173+
filter = Q(
174+
environment_id=environment_id,
175+
bucket_size=constants.ANALYTICS_READ_BUCKET_SIZE,
176+
feature_name=feature.name,
177+
created_at__date__lte=timezone.now(),
178+
created_at__date__gt=timezone.now() - timedelta(days=period_days),
179+
)
180+
if labels_filter:
181+
filter &= Q(labels__contains=labels_filter)
185182
feature_evaluation_data = (
186-
FeatureEvaluationBucket.objects.filter(
187-
environment_id=environment_id,
188-
bucket_size=constants.ANALYTICS_READ_BUCKET_SIZE,
189-
feature_name=feature.name,
190-
created_at__date__lte=timezone.now(),
191-
created_at__date__gt=timezone.now() - timedelta(days=period),
192-
)
183+
FeatureEvaluationBucket.objects.filter(filter)
193184
.order_by("created_at__date")
194-
.values("created_at__date", "feature_name", "environment_id")
185+
.values("created_at__date", "feature_name", "environment_id", "labels")
195186
.annotate(count=Sum("total_count"))
196187
)
197188
usage_list = []
@@ -200,15 +191,61 @@ def get_feature_evaluation_data_from_local_db(
200191
FeatureEvaluationData(
201192
day=data["created_at__date"],
202193
count=data["count"],
194+
labels=data["labels"],
203195
)
204196
)
205197
return usage_list
206198

207199

208-
def _get_environment_ids_for_org(organisation) -> List[int]: # type: ignore[no-untyped-def]
200+
def _get_environment_ids_for_org(organisation: Organisation) -> list[int]:
209201
# We need to do this to prevent Django from generating a query that
210202
# references the environments and projects tables,
211203
# as they do not exist in the analytics database.
212204
return [
213-
e.id for e in Environment.objects.filter(project__organisation=organisation)
205+
*Environment.objects.filter(
206+
project__organisation=organisation,
207+
).values_list(
208+
"id",
209+
flat=True,
210+
)
214211
]
212+
213+
214+
def _get_start_date_and_stop_date_for_subscribed_organisation(
215+
sub_cache: OrganisationSubscriptionInformationCache | None,
216+
period: PeriodType | None = None,
217+
) -> tuple[datetime | None, datetime | None]:
218+
"""
219+
Populate start and stop date for the given period
220+
from the organisation's subscription information.
221+
"""
222+
now = timezone.now()
223+
224+
match period:
225+
case constants.CURRENT_BILLING_PERIOD:
226+
if sub_cache and sub_cache.current_billing_term_starts_at:
227+
starts_at = sub_cache.current_billing_term_starts_at
228+
else:
229+
raise NotFound("No billing periods found for this organisation.")
230+
231+
month_delta = relativedelta(now, starts_at).months
232+
date_start = relativedelta(months=month_delta) + starts_at
233+
return date_start, now
234+
235+
case constants.PREVIOUS_BILLING_PERIOD:
236+
if sub_cache and sub_cache.current_billing_term_starts_at:
237+
starts_at = sub_cache.current_billing_term_starts_at
238+
else:
239+
raise NotFound("No billing periods found for this organisation.")
240+
241+
month_delta = relativedelta(now, starts_at).months - 1
242+
month_delta += relativedelta(now, starts_at).years * 12
243+
date_start = relativedelta(months=month_delta) + starts_at
244+
date_stop = relativedelta(months=month_delta + 1) + starts_at
245+
return date_start, date_stop
246+
247+
case constants.NINETY_DAY_PERIOD:
248+
date_start = now - relativedelta(days=90)
249+
return date_start, now
250+
251+
return None, None

0 commit comments

Comments
 (0)