Skip to content

Commit

Permalink
Merge branch 'master' into dev/add-tach
Browse files Browse the repository at this point in the history
  • Loading branch information
caelean authored Oct 16, 2024
2 parents 6d0b21e + 80499c8 commit 403f825
Show file tree
Hide file tree
Showing 205 changed files with 2,539 additions and 1,124 deletions.
31 changes: 15 additions & 16 deletions .github/workflows/copy-clickhouse-udfs.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
name: Trigger UDFs Workflow

on:
push:
branches:
- master
paths:
- 'posthog/user_scripts/**'
push:
branches:
- master
paths:
- 'posthog/user_scripts/**'

jobs:
trigger_udfs_workflow:
runs-on: ubuntu-latest
steps:
- name: Trigger UDFs Workflow
uses: benc-uk/workflow-dispatch@v1
with:
workflow: .github/workflows/clickhouse-udfs.yml
repo: posthog/posthog-cloud-infra
token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }}
ref: refs/heads/main

trigger_udfs_workflow:
runs-on: ubuntu-latest
steps:
- name: Trigger UDFs Workflow
uses: benc-uk/workflow-dispatch@v1
with:
workflow: .github/workflows/clickhouse-udfs.yml
repo: posthog/posthog-cloud-infra
token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }}
ref: refs/heads/main
2 changes: 2 additions & 0 deletions .storybook/decorators/withKea/kea-story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { worker } from '~/mocks/browser'
import { teamLogic } from 'scenes/teamLogic'
import { userLogic } from 'scenes/userLogic'
import { projectLogic } from 'scenes/projectLogic'

export function resetKeaStory(): void {
worker.resetHandlers()
Expand All @@ -18,6 +19,7 @@ export function resetKeaStory(): void {
initKea({ routerLocation: history.location, routerHistory: history })
featureFlagLogic.mount()
teamLogic.mount()
projectLogic.mount()
userLogic.mount()
router.mount()
const { store } = getContext()
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/billingUpgradeCTA.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ describe('Billing Upgrade CTA', () => {
cy.reload()

cy.get('[data-attr=billing-page-core-upgrade-cta] .LemonButton__content').should('not.exist')
cy.get('[data-attr=manage-billing]').should('have.text', 'Manage card details and view past invoices')
cy.get('[data-attr=manage-billing]').should('have.text', 'Manage card details and invoices')
})
})
2 changes: 1 addition & 1 deletion cypress/e2e/dashboard-duplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('duplicating dashboards', () => {
let dashboardName, insightName, expectedCopiedDashboardName, expectedCopiedInsightName

beforeEach(() => {
cy.intercept('POST', /\/api\/projects\/\d+\/dashboards/).as('createDashboard')
cy.intercept('POST', /\/api\/environments\/\d+\/dashboards/).as('createDashboard')

dashboardName = randomString('dashboard-')
expectedCopiedDashboardName = `${dashboardName} (Copy)`
Expand Down
6 changes: 3 additions & 3 deletions cypress/e2e/dashboard-shared.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { dashboards } from '../productAnalytics'

describe('Shared dashboard', () => {
beforeEach(() => {
cy.intercept('GET', /api\/projects\/\d+\/insights\/\?.*/).as('loadInsightList')
cy.intercept('PATCH', /api\/projects\/\d+\/insights\/\d+\/.*/).as('patchInsight')
cy.intercept('POST', /\/api\/projects\/\d+\/dashboards/).as('createDashboard')
cy.intercept('GET', /api\/environments\/\d+\/insights\/\?.*/).as('loadInsightList')
cy.intercept('PATCH', /api\/environments\/\d+\/insights\/\d+\/.*/).as('patchInsight')
cy.intercept('POST', /\/api\/environments\/\d+\/dashboards/).as('createDashboard')
cy.useSubscriptionStatus('unsubscribed')

cy.clickNavMenu('dashboards')
Expand Down
8 changes: 4 additions & 4 deletions cypress/e2e/dashboard.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { randomString } from '../support/random'

describe('Dashboard', () => {
beforeEach(() => {
cy.intercept('GET', /api\/projects\/\d+\/insights\/\?.*/).as('loadInsightList')
cy.intercept('PATCH', /api\/projects\/\d+\/insights\/\d+\/.*/).as('patchInsight')
cy.intercept('POST', /\/api\/projects\/\d+\/dashboards/).as('createDashboard')
cy.intercept('GET', /api\/environments\/\d+\/insights\/\?.*/).as('loadInsightList')
cy.intercept('PATCH', /api\/environments\/\d+\/insights\/\d+\/.*/).as('patchInsight')
cy.intercept('POST', /\/api\/environments\/\d+\/dashboards/).as('createDashboard')

cy.clickNavMenu('dashboards')
cy.location('pathname').should('include', '/dashboard')
Expand Down Expand Up @@ -306,7 +306,7 @@ describe('Dashboard', () => {
})

it('Move dashboard item', () => {
cy.intercept('PATCH', /api\/projects\/\d+\/dashboards\/\d+\/move_tile.*/).as('moveTile')
cy.intercept('PATCH', /api\/environments\/\d+\/dashboards\/\d+\/move_tile.*/).as('moveTile')

const sourceDashboard = randomString('source-dashboard')
const targetDashboard = randomString('target-dashboard')
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/insights-saved.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('Insights - saved', () => {
})

it('If cache empty, initiate async refresh', () => {
cy.intercept('GET', /\/api\/projects\/\d+\/insights\/?\?[^/]*?refresh=async/).as('getInsightsRefreshAsync')
cy.intercept('GET', /\/api\/environments\/\d+\/insights\/?\?[^/]*?refresh=async/).as('getInsightsRefreshAsync')
let newInsightId: string
createInsight('saved insight').then((insightId) => {
newInsightId = insightId
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/insights.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('Insights', () => {
})

it('Create new insight and save and continue editing', () => {
cy.intercept('PATCH', /\/api\/projects\/\d+\/insights\/\d+\/?/).as('patchInsight')
cy.intercept('PATCH', /\/api\/environments\/\d+\/insights\/\d+\/?/).as('patchInsight')

const insightName = randomString('insight-name-')
createInsight(insightName)
Expand Down
4 changes: 2 additions & 2 deletions cypress/e2e/notebooks.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { urls } from 'scenes/urls'
describe('Notebooks', () => {
beforeEach(() => {
cy.fixture('api/session-recordings/recordings.json').then((recordings) => {
cy.intercept('GET', /api\/projects\/\d+\/session_recordings\/?\?.*/, { body: recordings }).as(
cy.intercept('GET', /api\/environments\/\d+\/session_recordings\/?\?.*/, { body: recordings }).as(
'loadSessionRecordingsList'
)
})

cy.fixture('api/session-recordings/recording.json').then((recording) => {
cy.intercept('GET', /api\/projects\/\d+\/session_recordings\/.*\?.*/, { body: recording }).as(
cy.intercept('GET', /api\/environments\/\d+\/session_recordings\/.*\?.*/, { body: recording }).as(
'loadSessionRecording'
)
})
Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/projectHomepage.cy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
describe('Project Homepage', () => {
beforeEach(() => {
cy.intercept('GET', /\/api\/projects\/\d+\/dashboards\/\d+\//).as('getDashboard')
cy.intercept('GET', /\/api\/environments\/\d+\/dashboards\/\d+\//).as('getDashboard')
cy.clickNavMenu('projecthomepage')
})

Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/trends.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('Trends', () => {
})

it('Can load a graph from a URL directly', () => {
cy.intercept('POST', /api\/projects\/\d+\/query\//).as('loadNewQueryInsight')
cy.intercept('POST', /api\/environments\/\d+\/query\//).as('loadNewQueryInsight')

// regression test, the graph wouldn't load when going directly to a URL
cy.visit(
Expand Down
8 changes: 4 additions & 4 deletions cypress/productAnalytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const insight = {
cy.url().should('not.include', '/new')
},
clickTab: (tabName: string): void => {
cy.intercept('POST', /api\/projects\/\d+\/query\//).as('loadNewQueryInsight')
cy.intercept('POST', /api\/environments\/\d+\/query\//).as('loadNewQueryInsight')

cy.get(`[data-attr="insight-${(tabName === 'PATHS' ? 'PATH' : tabName).toLowerCase()}-tab"]`).click()
if (tabName !== 'FUNNELS') {
Expand All @@ -51,7 +51,7 @@ export const insight = {
}
},
newInsight: (insightType: string = 'TRENDS'): void => {
cy.intercept('POST', /api\/projects\/\d+\/query\//).as('loadNewQueryInsight')
cy.intercept('POST', /api\/environments\/\d+\/query\//).as('loadNewQueryInsight')

if (insightType === 'JSON') {
cy.clickNavMenu('savedinsights')
Expand Down Expand Up @@ -86,7 +86,7 @@ export const insight = {
cy.url().should('not.include', '/new') // wait for insight to complete and update URL
},
addInsightToDashboard: (dashboardName: string, options: { visitAfterAdding: boolean }): void => {
cy.intercept('PATCH', /api\/projects\/\d+\/insights\/\d+\/.*/).as('patchInsight')
cy.intercept('PATCH', /api\/environments\/\d+\/insights\/\d+\/.*/).as('patchInsight')

cy.get('[data-attr="save-to-dashboard-button"]').click()
cy.get('[data-attr="dashboard-searchfield"]').type(dashboardName)
Expand Down Expand Up @@ -158,7 +158,7 @@ export const dashboards = {

export const dashboard = {
addInsightToEmptyDashboard: (insightName: string): void => {
cy.intercept('POST', /api\/projects\/\d+\/insights\//).as('postInsight')
cy.intercept('POST', /api\/environments\/\d+\/insights\//).as('postInsight')

cy.get('[data-attr=dashboard-add-graph-header]').contains('Add insight').click()
cy.get('[data-attr=toast-close-button]').click({ multiple: true })
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ beforeEach(() => {
req.reply({ statusCode: 404, body: 'Cypress forced 404' })
)

cy.intercept('GET', /\/api\/projects\/\d+\/insights\/?\?/).as('getInsights')
cy.intercept('GET', /\/api\/environments\/\d+\/insights\/?\?/).as('getInsights')

cy.request('POST', '/api/login/', {
email: 'test@posthog.com',
Expand Down
2 changes: 2 additions & 0 deletions ee/clickhouse/queries/experiments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@

# Trends only: If p-value is below this threshold, the results are considered significant
P_VALUE_SIGNIFICANCE_LEVEL = 0.05

CONTROL_VARIANT_KEY = "control"
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from rest_framework.exceptions import ValidationError

from posthog.constants import ExperimentSignificanceCode, ExperimentNoResultsErrorKeys
from posthog.constants import ExperimentNoResultsErrorKeys
from posthog.hogql_queries.experiments import CONTROL_VARIANT_KEY
from posthog.hogql_queries.experiments.funnel_statistics import (
are_results_significant,
Expand All @@ -17,6 +17,7 @@
from posthog.models.filters.filter import Filter
from posthog.models.team import Team
from posthog.queries.funnels import ClickhouseFunnel
from posthog.schema import ExperimentSignificanceCode

Probability = float

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@

from flaky import flaky

from posthog.constants import ExperimentSignificanceCode
from posthog.hogql_queries.experiments.funnel_statistics import (
are_results_significant,
calculate_expected_loss,
calculate_probabilities,
calculate_credible_intervals as calculate_funnel_credible_intervals,
)
from posthog.schema import ExperimentVariantFunnelResult
from posthog.schema import ExperimentSignificanceCode, ExperimentVariantFunnelResult

Probability = float

Expand Down
33 changes: 16 additions & 17 deletions ee/clickhouse/queries/experiments/test_trend_experiment_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@

from flaky import flaky

from posthog.constants import ExperimentSignificanceCode
from posthog.hogql_queries.experiments.trend_statistics import (
are_results_significant,
calculate_credible_intervals,
calculate_p_value,
calculate_probabilities,
)
from posthog.schema import ExperimentVariantTrendResult
from posthog.schema import ExperimentSignificanceCode, ExperimentVariantTrendBaseStats

Probability = float

Expand All @@ -24,7 +23,7 @@ def logbeta(x: float, y: float) -> float:
# Helper function to calculate probability using a different method than the one used in actual code
# calculation: https://www.evanmiller.org/bayesian-ab-testing.html#count_ab
def calculate_probability_of_winning_for_target_count_data(
target_variant: ExperimentVariantTrendResult, other_variants: list[ExperimentVariantTrendResult]
target_variant: ExperimentVariantTrendBaseStats, other_variants: list[ExperimentVariantTrendBaseStats]
) -> Probability:
"""
Calculates the probability of winning for target variant.
Expand Down Expand Up @@ -98,8 +97,8 @@ def probability_C_beats_A_and_B_count_data(
@flaky(max_runs=10, min_passes=1)
class TestTrendExperimentCalculator(unittest.TestCase):
def test_calculate_results(self):
variant_control = ExperimentVariantTrendResult(key="A", count=20, exposure=1, absolute_exposure=200)
variant_test = ExperimentVariantTrendResult(key="B", count=30, exposure=1, absolute_exposure=200)
variant_control = ExperimentVariantTrendBaseStats(key="A", count=20, exposure=1, absolute_exposure=200)
variant_test = ExperimentVariantTrendBaseStats(key="B", count=30, exposure=1, absolute_exposure=200)

probabilities = calculate_probabilities(variant_control, [variant_test])
self.assertAlmostEqual(probabilities[1], 0.92, places=1)
Expand All @@ -118,8 +117,8 @@ def test_calculate_results(self):
self.assertAlmostEqual(credible_intervals[variant_test.key][1], 0.2141, places=3)

def test_calculate_results_small_numbers(self):
variant_control = ExperimentVariantTrendResult(key="A", count=2, exposure=1, absolute_exposure=200)
variant_test = ExperimentVariantTrendResult(key="B", count=1, exposure=1, absolute_exposure=200)
variant_control = ExperimentVariantTrendBaseStats(key="A", count=2, exposure=1, absolute_exposure=200)
variant_test = ExperimentVariantTrendBaseStats(key="B", count=1, exposure=1, absolute_exposure=200)

probabilities = calculate_probabilities(variant_control, [variant_test])
self.assertAlmostEqual(probabilities[1], 0.31, places=1)
Expand All @@ -146,9 +145,9 @@ def test_calculate_count_data_probability(self):
self.assertAlmostEqual(probability, probability2)

def test_calculate_results_with_three_variants(self):
variant_control = ExperimentVariantTrendResult(key="A", count=20, exposure=1, absolute_exposure=200)
variant_test_1 = ExperimentVariantTrendResult(key="B", count=26, exposure=1, absolute_exposure=200)
variant_test_2 = ExperimentVariantTrendResult(key="C", count=19, exposure=1, absolute_exposure=200)
variant_control = ExperimentVariantTrendBaseStats(key="A", count=20, exposure=1, absolute_exposure=200)
variant_test_1 = ExperimentVariantTrendBaseStats(key="B", count=26, exposure=1, absolute_exposure=200)
variant_test_2 = ExperimentVariantTrendBaseStats(key="C", count=19, exposure=1, absolute_exposure=200)

probabilities = calculate_probabilities(variant_control, [variant_test_1, variant_test_2])
self.assertAlmostEqual(probabilities[0], 0.16, places=1)
Expand All @@ -172,9 +171,9 @@ def test_calculate_results_with_three_variants(self):
self.assertAlmostEqual(credible_intervals[variant_test_2.key][1], 0.1484, places=3)

def test_calculate_significance_when_target_variants_underperform(self):
variant_control = ExperimentVariantTrendResult(key="A", count=250, exposure=1, absolute_exposure=200)
variant_test_1 = ExperimentVariantTrendResult(key="B", count=180, exposure=1, absolute_exposure=200)
variant_test_2 = ExperimentVariantTrendResult(key="C", count=50, exposure=1, absolute_exposure=200)
variant_control = ExperimentVariantTrendBaseStats(key="A", count=250, exposure=1, absolute_exposure=200)
variant_test_1 = ExperimentVariantTrendBaseStats(key="B", count=180, exposure=1, absolute_exposure=200)
variant_test_2 = ExperimentVariantTrendBaseStats(key="C", count=50, exposure=1, absolute_exposure=200)

# in this case, should choose B as best test variant
p_value = calculate_p_value(variant_control, [variant_test_1, variant_test_2])
Expand All @@ -188,7 +187,7 @@ def test_calculate_significance_when_target_variants_underperform(self):
self.assertEqual(significant, ExperimentSignificanceCode.LOW_WIN_PROBABILITY)

# new B variant is worse, such that control probability ought to be high enough
variant_test_1 = ExperimentVariantTrendResult(key="B", count=100, exposure=1, absolute_exposure=200)
variant_test_1 = ExperimentVariantTrendBaseStats(key="B", count=100, exposure=1, absolute_exposure=200)

significant, p_value = are_results_significant(
variant_control, [variant_test_1, variant_test_2], [0.95, 0.03, 0.02]
Expand All @@ -205,9 +204,9 @@ def test_calculate_significance_when_target_variants_underperform(self):
self.assertAlmostEqual(credible_intervals[variant_test_2.key][1], 0.3295, places=3)

def test_results_with_different_exposures(self):
variant_control = ExperimentVariantTrendResult(key="A", count=50, exposure=1.3, absolute_exposure=260)
variant_test_1 = ExperimentVariantTrendResult(key="B", count=30, exposure=1.8, absolute_exposure=360)
variant_test_2 = ExperimentVariantTrendResult(key="C", count=20, exposure=0.7, absolute_exposure=140)
variant_control = ExperimentVariantTrendBaseStats(key="A", count=50, exposure=1.3, absolute_exposure=260)
variant_test_1 = ExperimentVariantTrendBaseStats(key="B", count=30, exposure=1.8, absolute_exposure=360)
variant_test_2 = ExperimentVariantTrendBaseStats(key="C", count=20, exposure=0.7, absolute_exposure=140)

probabilities = calculate_probabilities(variant_control, [variant_test_1, variant_test_2]) # a is control
self.assertAlmostEqual(probabilities[0], 0.86, places=1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
TRENDS_CUMULATIVE,
TRENDS_LINEAR,
UNIQUE_USERS,
ExperimentSignificanceCode,
ExperimentNoResultsErrorKeys,
)
from posthog.hogql_queries.experiments.trend_statistics import (
Expand All @@ -28,6 +27,7 @@
from posthog.models.team import Team
from posthog.queries.trends.trends import Trends
from posthog.queries.trends.util import ALL_SUPPORTED_MATH_FUNCTIONS
from posthog.schema import ExperimentSignificanceCode

Probability = float

Expand Down
2 changes: 1 addition & 1 deletion ee/clickhouse/views/test/test_clickhouse_experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

from ee.api.test.base import APILicensedTest
from dateutil import parser
from posthog.constants import ExperimentSignificanceCode
from posthog.models.action.action import Action
from posthog.models.cohort.cohort import Cohort
from posthog.models.experiment import Experiment
from posthog.models.feature_flag import FeatureFlag, get_feature_flags_for_team_in_cache
from posthog.schema import ExperimentSignificanceCode
from posthog.test.base import (
ClickhouseTestMixin,
_create_event,
Expand Down
12 changes: 10 additions & 2 deletions ee/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def extend_api_router() -> None:
projects_router,
organizations_router,
project_feature_flags_router,
project_dashboards_router,
environment_dashboards_router,
legacy_project_dashboards_router,
)

root_router.register(r"billing", billing.BillingViewset, "billing")
Expand Down Expand Up @@ -67,7 +68,14 @@ def extend_api_router() -> None:
"environment_explicit_members",
["team_id"],
)
project_dashboards_router.register(

environment_dashboards_router.register(
r"collaborators",
dashboard_collaborator.DashboardCollaboratorViewSet,
"environment_dashboard_collaborators",
["project_id", "dashboard_id"],
)
legacy_project_dashboards_router.register(
r"collaborators",
dashboard_collaborator.DashboardCollaboratorViewSet,
"project_dashboard_collaborators",
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.
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.
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.
Binary file modified frontend/__snapshots__/scenes-app-max-ai--welcome--dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified frontend/__snapshots__/scenes-app-max-ai--welcome--light.png
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.
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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified frontend/__snapshots__/scenes-other-billing--billing--dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified frontend/__snapshots__/scenes-other-billing--billing--light.png
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.
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.
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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 403f825

Please sign in to comment.