Skip to content

Commit

Permalink
feat: no limit on playlists for free users (#27454)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
pauldambra and github-actions[bot] authored Jan 15, 2025
1 parent 6de4d02 commit cf9b398
Show file tree
Hide file tree
Showing 10 changed files with 19 additions and 95 deletions.
12 changes: 0 additions & 12 deletions ee/session_recordings/session_recording_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,14 @@
from loginas.utils import is_impersonated_session
from rest_framework import request, response, serializers, viewsets
from posthog.api.utils import action
from rest_framework.exceptions import PermissionDenied

from posthog.api.forbid_destroy_model import ForbidDestroyModel
from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.api.shared import UserBasicSerializer
from posthog.constants import AvailableFeature
from posthog.models import (
SessionRecording,
SessionRecordingPlaylist,
SessionRecordingPlaylistItem,
Team,
User,
)
from posthog.models.activity_logging.activity_log import (
Expand All @@ -26,7 +23,6 @@
changes_between,
log_activity,
)
from posthog.models.team.team import check_is_feature_available_for_team
from posthog.models.utils import UUIDT
from posthog.rate_limit import (
ClickHouseBurstRateThrottle,
Expand Down Expand Up @@ -108,8 +104,6 @@ def create(self, validated_data: dict, *args, **kwargs) -> SessionRecordingPlayl
request = self.context["request"]
team = self.context["get_team"]()

self._check_can_create_playlist(team)

created_by = validated_data.pop("created_by", request.user)
playlist = SessionRecordingPlaylist.objects.create(
team=team,
Expand Down Expand Up @@ -158,12 +152,6 @@ def update(self, instance: SessionRecordingPlaylist, validated_data: dict, **kwa

return updated_playlist

def _check_can_create_playlist(self, team: Team) -> bool:
playlist_count = SessionRecordingPlaylist.objects.filter(deleted=False, team=team).count()
if not check_is_feature_available_for_team(team.pk, AvailableFeature.RECORDINGS_PLAYLISTS, playlist_count):
raise PermissionDenied("You have hit the limit for playlists for this team.")
return True


class SessionRecordingPlaylistViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet):
scope_object = "session_recording_playlist"
Expand Down
18 changes: 3 additions & 15 deletions ee/session_recordings/test/test_session_recording_playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from rest_framework import status

from ee.api.test.base import APILicensedTest
from ee.api.test.fixtures.available_product_features import AVAILABLE_PRODUCT_FEATURES
from posthog.models import SessionRecording, SessionRecordingPlaylistItem
from posthog.models.user import User
from posthog.session_recordings.models.session_recording_playlist import (
Expand Down Expand Up @@ -77,24 +76,13 @@ def test_creates_playlist(self):
"last_modified_by": response.json()["last_modified_by"],
}

def test_creates_too_many_playlists(self):
limit = 0
self.organization.available_product_features = AVAILABLE_PRODUCT_FEATURES
self.organization.save()
for feature in AVAILABLE_PRODUCT_FEATURES:
if "key" in feature and feature["key"] == "recordings_playlists":
limit = int(feature["limit"])
for _ in range(limit):
def test_can_create_many_playlists(self):
for i in range(100):
response = self.client.post(
f"/api/projects/{self.team.id}/session_recording_playlists",
data={"name": "test"},
data={"name": f"test-{i}"},
)
assert response.status_code == status.HTTP_201_CREATED
response = self.client.post(
f"/api/projects/{self.team.id}/session_recording_playlists",
data={"name": "test"},
)
assert response.status_code == status.HTTP_403_FORBIDDEN

def test_gets_individual_playlist_by_shortid(self):
create_response = self.client.post(f"/api/projects/{self.team.id}/session_recording_playlists")
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ export const PayGateMiniLimitFeatureOther = (): JSX.Element => {
...meCurrent.organization,
available_product_features: [
{
key: 'recordings_playlists',
name: 'Recordings Playlists',
key: 'advanced_permissions',
name: 'Advanced Permissions',
limit: 3,
},
],
Expand All @@ -93,7 +93,7 @@ export const PayGateMiniLimitFeatureOther = (): JSX.Element => {
],
},
})
return <Template feature={AvailableFeature.RECORDINGS_PLAYLISTS} currentUsage={3} />
return <Template feature={AvailableFeature.ADVANCED_PERMISSIONS} currentUsage={3} />
}

export const PayGateMiniLimitFeatureProjects = (): JSX.Element => {
Expand Down
27 changes: 0 additions & 27 deletions frontend/src/mocks/fixtures/_billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -922,15 +922,6 @@ export const billingJson: BillingType = {
limit: null,
note: null,
},
{
key: 'recordings_playlists',
name: 'Recording playlists',
description:
'Create playlists of certain session recordings to easily find and watch them again in the future.',
unit: 'playlists',
limit: 5,
note: null,
},
{
key: 'session_replay_data_retention',
name: 'Data retention',
Expand Down Expand Up @@ -1041,15 +1032,6 @@ export const billingJson: BillingType = {
limit: null,
note: null,
},
{
key: 'recordings_playlists',
name: 'Recording playlists',
description:
'Create playlists of certain session recordings to easily find and watch them again in the future.',
unit: null,
limit: null,
note: null,
},
{
key: 'recordings_performance',
name: 'Network performance on recordings',
Expand Down Expand Up @@ -1293,15 +1275,6 @@ export const billingJson: BillingType = {
contact_support: false,
inclusion_only: false,
features: [
{
key: 'recordings_playlists',
name: 'Recording playlists',
description:
'Create playlists of certain session recordings to easily find and watch them again in the future.',
images: null,
icon_key: null,
type: null,
},
{
key: 'session_replay_data_retention',
name: 'Data retention',
Expand Down
31 changes: 7 additions & 24 deletions frontend/src/scenes/session-recordings/SessionRecordings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
defaultAuthorizedUrlProperties,
} from 'lib/components/AuthorizedUrlList/authorizedUrlListLogic'
import { PageHeader } from 'lib/components/PageHeader'
import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic'
import { VersionCheckerBanner } from 'lib/components/VersionChecker/VersionCheckerBanner'
import { FEATURE_FLAGS } from 'lib/constants'
import { useAsyncHandler } from 'lib/hooks/useAsyncHandler'
Expand All @@ -24,19 +23,15 @@ import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'

import { sidePanelSettingsLogic } from '~/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic'
import { AvailableFeature, NotebookNodeType, ReplayTabs } from '~/types'
import { NotebookNodeType, ReplayTabs } from '~/types'

import { createPlaylist } from './playlist/playlistUtils'
import { SessionRecordingsPlaylist } from './playlist/SessionRecordingsPlaylist'
import { SavedSessionRecordingPlaylists } from './saved-playlists/SavedSessionRecordingPlaylists'
import { savedSessionRecordingPlaylistsLogic } from './saved-playlists/savedSessionRecordingPlaylistsLogic'
import { humanFriendlyTabName, sessionReplaySceneLogic } from './sessionReplaySceneLogic'
import SessionRecordingTemplates from './templates/SessionRecordingTemplates'

function Header(): JSX.Element {
const { guardAvailableFeature } = useValues(upgradeModalLogic)
const playlistsLogic = savedSessionRecordingPlaylistsLogic({ tab: ReplayTabs.Home })
const { playlists } = useValues(playlistsLogic)
const { tab } = useValues(sessionReplaySceneLogic)
const { currentTeam } = useValues(teamLogic)
const recordingsDisabled = currentTeam && !currentTeam?.session_recording_opt_in
Expand Down Expand Up @@ -84,17 +79,11 @@ function Header(): JSX.Element {
data-attr="session-recordings-filters-save-as-playlist"
type="primary"
onClick={(e) =>
guardAvailableFeature(
AvailableFeature.RECORDINGS_PLAYLISTS,
() => {
// choose the type of playlist handler so that analytics correctly report
// whether filters have been changed before saving
totalFiltersCount === 0
? newPlaylistHandler.onEvent?.(e)
: saveFiltersPlaylistHandler.onEvent?.(e)
},
{ currentUsage: playlists.count }
)
// choose the type of playlist handler so that analytics correctly report
// whether filters have been changed before saving
totalFiltersCount === 0
? newPlaylistHandler.onEvent?.(e)
: saveFiltersPlaylistHandler.onEvent?.(e)
}
>
Save as playlist
Expand All @@ -112,13 +101,7 @@ function Header(): JSX.Element {
{tab === ReplayTabs.Playlists && (
<LemonButton
type="primary"
onClick={(e) =>
guardAvailableFeature(
AvailableFeature.RECORDINGS_PLAYLISTS,
() => newPlaylistHandler.onEvent?.(e),
{ currentUsage: playlists.count }
)
}
onClick={(e) => newPlaylistHandler.onEvent?.(e)}
data-attr="save-recordings-playlist-button"
loading={newPlaylistHandler.loading}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { IconPlus } from '@posthog/icons'
import { useValues } from 'kea'
import { upgradeModalLogic } from 'lib/components/UpgradeModal/upgradeModalLogic'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { LemonButton } from 'lib/lemon-ui/LemonButton'

import { AvailableFeature, ReplayTabs } from '~/types'
import { ReplayTabs } from '~/types'

import { createPlaylist } from '../playlist/playlistUtils'
import { savedSessionRecordingPlaylistsLogic } from './savedSessionRecordingPlaylistsLogic'

export function SavedSessionRecordingPlaylistsEmptyState(): JSX.Element {
const { guardAvailableFeature } = useValues(upgradeModalLogic)
const playlistsLogic = savedSessionRecordingPlaylistsLogic({ tab: ReplayTabs.Home })
const { playlists, loadPlaylistsFailed } = useValues(playlistsLogic)
const { loadPlaylistsFailed } = useValues(playlistsLogic)
return loadPlaylistsFailed ? (
<LemonBanner type="error">Error while trying to load playlist.</LemonBanner>
) : (
Expand All @@ -24,13 +22,7 @@ export function SavedSessionRecordingPlaylistsEmptyState(): JSX.Element {
type="primary"
data-attr="add-session-playlist-button-empty-state"
icon={<IconPlus />}
onClick={() =>
guardAvailableFeature(
AvailableFeature.RECORDINGS_PLAYLISTS,
() => void createPlaylist({}, true),
{ currentUsage: playlists.count }
)
}
onClick={() => void createPlaylist({}, true)}
>
New playlist
</LemonButton>
Expand Down
4 changes: 2 additions & 2 deletions posthog/session_recordings/queries/session_replay_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ def get_events(

def ttl_days(team: Team) -> int:
if is_cloud():
# NOTE: We use Playlists as a proxy to see if they are subbed to Recordings
is_paid = team.organization.is_feature_available(AvailableFeature.RECORDINGS_PLAYLISTS)
# NOTE: We use file export as a proxy to see if they are subbed to Recordings
is_paid = team.organization.is_feature_available(AvailableFeature.RECORDINGS_FILE_EXPORT)
ttl_days = settings.REPLAY_RETENTION_DAYS_MAX if is_paid else settings.REPLAY_RETENTION_DAYS_MIN

# NOTE: The date we started reliably ingested data to blob storage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -891,7 +891,7 @@ def test_ttl_days(self):
assert ttl_days(self.team) == 30

self.team.organization.available_product_features = [
{"key": AvailableFeature.RECORDINGS_PLAYLISTS, "name": AvailableFeature.RECORDINGS_PLAYLISTS}
{"key": AvailableFeature.RECORDINGS_FILE_EXPORT, "name": AvailableFeature.RECORDINGS_FILE_EXPORT}
]

# Far enough in the future from `days_since_blob_ingestion` but paid
Expand Down

0 comments on commit cf9b398

Please sign in to comment.