Add weekly active users count in anonymous usage stats (#6317)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Added the weekly active users count to anonymous usage statistics.
|
||||
@@ -53,6 +53,7 @@ func TestMaybeSendStatistics(t *testing.T) {
|
||||
VulnDetectionEnabled: true,
|
||||
SystemUsersEnabled: true,
|
||||
HostsStatusWebHookEnabled: true,
|
||||
NumWeeklyActiveUsers: 111,
|
||||
}, true, nil
|
||||
}
|
||||
recorded := false
|
||||
@@ -64,7 +65,7 @@ func TestMaybeSendStatistics(t *testing.T) {
|
||||
err := trySendStatistics(context.Background(), ds, fleet.StatisticsFrequency, ts.URL, &fleet.LicenseInfo{Tier: "premium"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, recorded)
|
||||
assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","numHostsEnrolled":999,"numUsers":99,"numTeams":9,"numPolicies":0,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true}`, requestBody)
|
||||
assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","numHostsEnrolled":999,"numUsers":99,"numTeams":9,"numPolicies":0,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true,"numWeeklyActiveUsers":111}`, requestBody)
|
||||
}
|
||||
|
||||
func TestMaybeSendStatisticsSkipsSendingIfNotNeeded(t *testing.T) {
|
||||
|
||||
@@ -15,15 +15,16 @@ Fleet Device Management Inc. periodically collects anonymous information about y
|
||||
"anonymousIdentifier": "9pnzNmrES3mQG66UQtd29cYTiX2+fZ4CYxDvh495720=",
|
||||
"fleetVersion": "x.x.x",
|
||||
"licenseTier": "free",
|
||||
"numHostsEnrolled": 12345,
|
||||
"numUsers": 12,
|
||||
"numTeams": 3,
|
||||
"numPolicies": 5,
|
||||
"numLabels": 20,
|
||||
"numHostsEnrolled": 999,
|
||||
"numUsers": 999,
|
||||
"numTeams": 999,
|
||||
"numPolicies": 999,
|
||||
"numLabels": 999,
|
||||
"softwareInventoryEnabled": true,
|
||||
"vulnDetectionEnabled": true,
|
||||
"systemUsersEnabled": true,
|
||||
"hostStatusWebhookEnabled": true,
|
||||
"numWeeklyActiveUsers": 999
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -70,15 +70,16 @@ export const usageStatsPreview = {
|
||||
anonymousIdentifier: "9pnzNmrES3mQG66UQtd29cYTiX2+fZ4CYxDvh495720=",
|
||||
fleetVersion: "x.x.x",
|
||||
licenseTier: "free",
|
||||
numHostsEnrolled: 12345,
|
||||
numUsers: 12,
|
||||
numTeams: 3,
|
||||
numPolicies: 5,
|
||||
numLabels: 20,
|
||||
numHostsEnrolled: 999,
|
||||
numUsers: 999,
|
||||
numTeams: 999,
|
||||
numPolicies: 999,
|
||||
numLabels: 999,
|
||||
softwareInventoryEnabled: true,
|
||||
vulnDetectionEnabled: true,
|
||||
systemUsersEnabled: true,
|
||||
hostStatusWebhookEnabled: true,
|
||||
numWeeklyActiveUsers: 999,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -18,33 +18,51 @@ type statistics struct {
|
||||
}
|
||||
|
||||
func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Duration, license *fleet.LicenseInfo) (fleet.StatisticsPayload, bool, error) {
|
||||
amountEnrolledHosts, err := amountEnrolledHostsDB(ctx, ds.writer)
|
||||
if err != nil {
|
||||
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "amount enrolled hosts")
|
||||
}
|
||||
amountUsers, err := amountUsersDB(ctx, ds.writer)
|
||||
if err != nil {
|
||||
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "amount users")
|
||||
}
|
||||
amountTeams, err := amountTeamsDB(ctx, ds.writer)
|
||||
if err != nil {
|
||||
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "amount teams")
|
||||
}
|
||||
amountPolicies, err := amountPoliciesDB(ctx, ds.writer)
|
||||
if err != nil {
|
||||
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "amount policies")
|
||||
}
|
||||
amountLabels, err := amountLabelsDB(ctx, ds.writer)
|
||||
if err != nil {
|
||||
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "amount labels")
|
||||
}
|
||||
appConfig, err := ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "statistics app config")
|
||||
computeStats := func(stats *fleet.StatisticsPayload, since time.Time) error {
|
||||
amountEnrolledHosts, err := amountEnrolledHostsDB(ctx, ds.writer)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount enrolled hosts")
|
||||
}
|
||||
amountUsers, err := amountUsersDB(ctx, ds.writer)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount users")
|
||||
}
|
||||
amountTeams, err := amountTeamsDB(ctx, ds.writer)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount teams")
|
||||
}
|
||||
amountPolicies, err := amountPoliciesDB(ctx, ds.writer)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount policies")
|
||||
}
|
||||
amountLabels, err := amountLabelsDB(ctx, ds.writer)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount labels")
|
||||
}
|
||||
appConfig, err := ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "statistics app config")
|
||||
}
|
||||
amountWeeklyUsers, err := amountActiveUsersSinceDB(ctx, ds.writer, since)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "amount active users")
|
||||
}
|
||||
|
||||
stats.NumHostsEnrolled = amountEnrolledHosts
|
||||
stats.NumUsers = amountUsers
|
||||
stats.NumTeams = amountTeams
|
||||
stats.NumPolicies = amountPolicies
|
||||
stats.NumLabels = amountLabels
|
||||
stats.SoftwareInventoryEnabled = appConfig.HostSettings.EnableSoftwareInventory
|
||||
stats.VulnDetectionEnabled = appConfig.VulnerabilitySettings.DatabasesPath != ""
|
||||
stats.SystemUsersEnabled = appConfig.HostSettings.EnableHostUsers
|
||||
stats.HostsStatusWebHookEnabled = appConfig.WebhookSettings.HostStatusWebhook.Enable
|
||||
stats.NumWeeklyActiveUsers = amountWeeklyUsers
|
||||
return nil
|
||||
}
|
||||
|
||||
dest := statistics{}
|
||||
err = sqlx.GetContext(ctx, ds.writer, &dest, `SELECT created_at, updated_at, anonymous_identifier FROM statistics LIMIT 1`)
|
||||
err := sqlx.GetContext(ctx, ds.writer, &dest, `SELECT created_at, updated_at, anonymous_identifier FROM statistics LIMIT 1`)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
anonIdentifier, err := server.GenerateRandomText(64)
|
||||
@@ -55,23 +73,22 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
|
||||
if err != nil {
|
||||
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "insert statistics")
|
||||
}
|
||||
return fleet.StatisticsPayload{
|
||||
AnonymousIdentifier: anonIdentifier,
|
||||
FleetVersion: version.Version().Version,
|
||||
LicenseTier: license.Tier,
|
||||
NumHostsEnrolled: amountEnrolledHosts,
|
||||
NumUsers: amountUsers,
|
||||
NumTeams: amountTeams,
|
||||
NumPolicies: amountPolicies,
|
||||
NumLabels: amountLabels,
|
||||
SoftwareInventoryEnabled: appConfig.HostSettings.EnableSoftwareInventory,
|
||||
VulnDetectionEnabled: appConfig.VulnerabilitySettings.DatabasesPath != "",
|
||||
SystemUsersEnabled: appConfig.HostSettings.EnableHostUsers,
|
||||
HostsStatusWebHookEnabled: appConfig.WebhookSettings.HostStatusWebhook.Enable,
|
||||
}, true, nil
|
||||
|
||||
// compute active weekly users since now - frequency
|
||||
stats := fleet.StatisticsPayload{
|
||||
AnonymousIdentifier: anonIdentifier,
|
||||
FleetVersion: version.Version().Version,
|
||||
LicenseTier: license.Tier,
|
||||
}
|
||||
if err := computeStats(&stats, time.Now().Add(-frequency)); err != nil {
|
||||
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "compute statistics")
|
||||
}
|
||||
|
||||
return stats, true, nil
|
||||
}
|
||||
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "get statistics")
|
||||
}
|
||||
|
||||
lastUpdated := dest.UpdatedAt
|
||||
if dest.CreatedAt.After(dest.UpdatedAt) {
|
||||
lastUpdated = dest.CreatedAt
|
||||
@@ -79,20 +96,17 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
|
||||
if time.Now().Before(lastUpdated.Add(frequency)) {
|
||||
return fleet.StatisticsPayload{}, false, nil
|
||||
}
|
||||
return fleet.StatisticsPayload{
|
||||
AnonymousIdentifier: dest.Identifier,
|
||||
FleetVersion: version.Version().Version,
|
||||
LicenseTier: license.Tier,
|
||||
NumHostsEnrolled: amountEnrolledHosts,
|
||||
NumUsers: amountUsers,
|
||||
NumTeams: amountTeams,
|
||||
NumPolicies: amountPolicies,
|
||||
NumLabels: amountLabels,
|
||||
SoftwareInventoryEnabled: appConfig.HostSettings.EnableSoftwareInventory,
|
||||
VulnDetectionEnabled: appConfig.VulnerabilitySettings.DatabasesPath != "",
|
||||
SystemUsersEnabled: appConfig.HostSettings.EnableHostUsers,
|
||||
HostsStatusWebHookEnabled: appConfig.WebhookSettings.HostStatusWebhook.Enable,
|
||||
}, true, nil
|
||||
|
||||
stats := fleet.StatisticsPayload{
|
||||
AnonymousIdentifier: dest.Identifier,
|
||||
FleetVersion: version.Version().Version,
|
||||
LicenseTier: license.Tier,
|
||||
}
|
||||
if err := computeStats(&stats, lastUpdated); err != nil {
|
||||
return fleet.StatisticsPayload{}, false, ctxerr.Wrap(ctx, err, "compute statistics")
|
||||
}
|
||||
|
||||
return stats, true, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) RecordStatisticsSent(ctx context.Context) error {
|
||||
|
||||
@@ -44,8 +44,8 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create new user for test
|
||||
_, err = ds.NewUser(context.Background(), &fleet.User{
|
||||
// Create two new users for test
|
||||
u1, err := ds.NewUser(context.Background(), &fleet.User{
|
||||
Password: []byte("foobar"),
|
||||
AdminForcedPasswordReset: false,
|
||||
Email: "baz@example.com",
|
||||
@@ -53,6 +53,17 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
GlobalRole: ptr.String(fleet.RoleObserver),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = ds.NewUser(context.Background(), &fleet.User{
|
||||
Password: []byte("foobar"),
|
||||
AdminForcedPasswordReset: false,
|
||||
Email: "qux@example.com",
|
||||
SSOEnabled: false,
|
||||
GlobalRole: ptr.String(fleet.RoleObserver),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// Create a session for user baz, but not qux (so only 1 is active)
|
||||
_, err = ds.NewSession(context.Background(), u1.ID, "session_key")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create new team for test
|
||||
_, err = ds.NewTeam(context.Background(), &fleet.Team{
|
||||
@@ -95,6 +106,8 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
err = ds.SaveAppConfig(context.Background(), config)
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(1100 * time.Millisecond) // ensure the DB timestamp is not in the same second
|
||||
|
||||
license := &fleet.LicenseInfo{Tier: "premium"}
|
||||
|
||||
// First time running, we send statistics
|
||||
@@ -105,7 +118,7 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
assert.NotEmpty(t, stats.FleetVersion)
|
||||
assert.Equal(t, stats.LicenseTier, "premium")
|
||||
assert.Equal(t, stats.NumHostsEnrolled, 1)
|
||||
assert.Equal(t, stats.NumUsers, 1)
|
||||
assert.Equal(t, stats.NumUsers, 2)
|
||||
assert.Equal(t, stats.NumTeams, 1)
|
||||
assert.Equal(t, stats.NumPolicies, 1)
|
||||
assert.Equal(t, stats.NumLabels, 1)
|
||||
@@ -113,6 +126,7 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
assert.Equal(t, stats.SystemUsersEnabled, false)
|
||||
assert.Equal(t, stats.VulnDetectionEnabled, false)
|
||||
assert.Equal(t, stats.HostsStatusWebHookEnabled, true)
|
||||
assert.Equal(t, stats.NumWeeklyActiveUsers, 1)
|
||||
|
||||
firstIdentifier := stats.AnonymousIdentifier
|
||||
|
||||
@@ -124,7 +138,7 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
assert.False(t, shouldSend)
|
||||
|
||||
time.Sleep(2)
|
||||
time.Sleep(1100 * time.Millisecond) // ensure the DB timestamp is not in the same second
|
||||
|
||||
_, err = ds.NewHost(context.Background(), &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
@@ -146,4 +160,25 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
assert.True(t, shouldSend)
|
||||
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
|
||||
assert.Equal(t, stats.NumHostsEnrolled, 2)
|
||||
assert.Equal(t, stats.NumUsers, 2)
|
||||
assert.Equal(t, stats.NumWeeklyActiveUsers, 0) // no active user since last stats were sent
|
||||
|
||||
// Create multiple new sessions for a single user
|
||||
_, err = ds.NewSession(context.Background(), u1.ID, "session_key2")
|
||||
require.NoError(t, err)
|
||||
_, err = ds.NewSession(context.Background(), u1.ID, "session_key3")
|
||||
require.NoError(t, err)
|
||||
_, err = ds.NewSession(context.Background(), u1.ID, "session_key4")
|
||||
require.NoError(t, err)
|
||||
|
||||
// wait a bit and resend statistics
|
||||
time.Sleep(1100 * time.Millisecond) // ensure the DB timestamp is not in the same second
|
||||
|
||||
stats, shouldSend, err = ds.ShouldSendStatistics(context.Background(), time.Millisecond, license)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, shouldSend)
|
||||
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
|
||||
assert.Equal(t, stats.NumHostsEnrolled, 2)
|
||||
assert.Equal(t, stats.NumUsers, 2)
|
||||
assert.Equal(t, stats.NumWeeklyActiveUsers, 1)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
@@ -283,3 +284,20 @@ func amountUsersDB(ctx context.Context, db sqlx.QueryerContext) (int, error) {
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
func amountActiveUsersSinceDB(ctx context.Context, db sqlx.QueryerContext, since time.Time) (int, error) {
|
||||
var amount int
|
||||
err := sqlx.GetContext(ctx, db, &amount, `
|
||||
SELECT count(*)
|
||||
FROM users u
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM sessions ssn
|
||||
WHERE ssn.user_id = u.id AND
|
||||
ssn.accessed_at >= ?
|
||||
)`, since)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return amount, nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type StatisticsPayload struct {
|
||||
VulnDetectionEnabled bool `json:"vulnDetectionEnabled"`
|
||||
SystemUsersEnabled bool `json:"systemUsersEnabled"`
|
||||
HostsStatusWebHookEnabled bool `json:"hostsStatusWebHookEnabled"`
|
||||
NumWeeklyActiveUsers int `json:"numWeeklyActiveUsers"`
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -20,6 +20,7 @@ module.exports = {
|
||||
vulnDetectionEnabled: { type: 'boolean', defaultsTo: false },
|
||||
systemUsersEnabled: { type: 'boolean', defaultsTo: false },
|
||||
hostStatusWebhookEnabled: { type: 'boolean', defaultsTo: false },
|
||||
numWeeklyActiveUsers: { type: 'number', defaultsTo: 0 },
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ module.exports = {
|
||||
vulnDetectionEnabled: { required: true, type: 'boolean' },
|
||||
systemUsersEnabled: { required: true, type: 'boolean' },
|
||||
hostStatusWebhookEnabled: { required: true, type: 'boolean' },
|
||||
numWeeklyActiveUsers: { required: true, type: 'number' },
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
|
||||
Vendored
+1
-1
@@ -13,7 +13,7 @@
|
||||
Cloud.setup({
|
||||
|
||||
/* eslint-disable */
|
||||
methods: {"downloadSitemap":{"verb":"GET","url":"/sitemap.xml","args":[]},"receiveUsageAnalytics":{"verb":"POST","url":"/api/v1/webhooks/receive-usage-analytics","args":["anonymousIdentifier","fleetVersion","licenseTier","numHostsEnrolled","numUsers","numTeams","numPolicies","numLabels","softwareInventoryEnabled","vulnDetectionEnabled","systemUsersEnabled","hostStatusWebhookEnabled"]},"receiveFromGithub":{"verb":"GET","url":"/api/v1/webhooks/github","args":["botSignature","action","sender","repository","changes","issue","comment","pull_request","label"]},"deliverContactFormMessage":{"verb":"POST","url":"/api/v1/deliver-contact-form-message","args":["emailAddress","topic","firstName","lastName","message"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"signup":{"verb":"POST","url":"/api/v1/customers/signup","args":["emailAddress","password","organization","firstName","lastName"]},"updateProfile":{"verb":"POST","url":"/api/v1/account/update-profile","args":["firstName","lastName","organization","emailAddress"]},"updatePassword":{"verb":"POST","url":"/api/v1/account/update-password","args":["oldPassword","newPassword"]},"updateBillingCard":{"verb":"POST","url":"/api/v1/account/update-billing-card","args":["stripeToken","billingCardLast4","billingCardBrand","billingCardExpMonth","billingCardExpYear"]},"login":{"verb":"POST","url":"/api/v1/customers/login","args":["emailAddress","password","rememberMe"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"createQuote":{"verb":"POST","url":"/api/v1/customers/create-quote","args":["numberOfHosts"]},"saveBillingInfoAndSubscribe":{"verb":"POST","url":"/api/v1/customers/save-billing-info-and-subscribe","args":["quoteId","paymentSource"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deliverDemoSignup":{"verb":"POST","url":"/api/v1/deliver-demo-signup","args":[]}}
|
||||
methods: {"downloadSitemap":{"verb":"GET","url":"/sitemap.xml","args":[]},"receiveUsageAnalytics":{"verb":"POST","url":"/api/v1/webhooks/receive-usage-analytics","args":["anonymousIdentifier","fleetVersion","licenseTier","numHostsEnrolled","numUsers","numTeams","numPolicies","numLabels","softwareInventoryEnabled","vulnDetectionEnabled","systemUsersEnabled","hostStatusWebhookEnabled", "numWeeklyActiveUsers"]},"receiveFromGithub":{"verb":"GET","url":"/api/v1/webhooks/github","args":["botSignature","action","sender","repository","changes","issue","comment","pull_request","label"]},"deliverContactFormMessage":{"verb":"POST","url":"/api/v1/deliver-contact-form-message","args":["emailAddress","topic","firstName","lastName","message"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"signup":{"verb":"POST","url":"/api/v1/customers/signup","args":["emailAddress","password","organization","firstName","lastName"]},"updateProfile":{"verb":"POST","url":"/api/v1/account/update-profile","args":["firstName","lastName","organization","emailAddress"]},"updatePassword":{"verb":"POST","url":"/api/v1/account/update-password","args":["oldPassword","newPassword"]},"updateBillingCard":{"verb":"POST","url":"/api/v1/account/update-billing-card","args":["stripeToken","billingCardLast4","billingCardBrand","billingCardExpMonth","billingCardExpYear"]},"login":{"verb":"POST","url":"/api/v1/customers/login","args":["emailAddress","password","rememberMe"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"createQuote":{"verb":"POST","url":"/api/v1/customers/create-quote","args":["numberOfHosts"]},"saveBillingInfoAndSubscribe":{"verb":"POST","url":"/api/v1/customers/save-billing-info-and-subscribe","args":["quoteId","paymentSource"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deliverDemoSignup":{"verb":"POST","url":"/api/v1/deliver-demo-signup","args":[]}}
|
||||
/* eslint-enable */
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user