Skip to content

Commit

Permalink
fix: telemetry and stats fix (#732)
Browse files Browse the repository at this point in the history
* fix: telemetry and license stats fix

* test: featureFlag

* fix: tests

* chore: changelog

* fix: telemetry
  • Loading branch information
sattvikc authored Jun 30, 2023
1 parent 025379f commit 7358d22
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Updated POST `/appid-<appId>/<tenantId>/recipe/session/remove`
- Adds `revokeAcrossAllTenants` with default `true` - controls revoking of sessions across all tenants or only a
particular tenant
- Updated telemetry to send `connectionUriDomain`, `appId` and `mau` information
- Updated feature flag stats to report `usersCount` per tenant

## [6.0.0] - 2023-06-02

Expand Down
17 changes: 16 additions & 1 deletion ee/src/main/java/io/supertokens/ee/EEFeatureFlag.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.supertokens.cronjobs.telemetry.Telemetry;
import io.supertokens.ee.cronjobs.EELicenseCheck;
import io.supertokens.featureflag.EE_FEATURES;
import io.supertokens.featureflag.FeatureFlag;
import io.supertokens.featureflag.exceptions.InvalidLicenseKeyException;
import io.supertokens.featureflag.exceptions.NoLicenseKeyFoundException;
import io.supertokens.httpRequest.HttpRequest;
Expand All @@ -28,6 +29,7 @@
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.TenantConfig;
import io.supertokens.pluginInterface.multitenancy.TenantIdentifier;
import io.supertokens.pluginInterface.multitenancy.ThirdPartyConfig;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage;
Expand Down Expand Up @@ -231,8 +233,10 @@ private JsonObject getMultiTenancyStats()

{
Storage storage = StorageLayer.getStorage(tenantConfig.tenantIdentifier, main);
boolean hasUsersOrSessions = ((AuthRecipeStorage) storage).getUsersCount(tenantConfig.tenantIdentifier, null) > 0;
long usersCount = ((AuthRecipeStorage) storage).getUsersCount(tenantConfig.tenantIdentifier, null);
boolean hasUsersOrSessions = (usersCount > 0);
hasUsersOrSessions = hasUsersOrSessions || ((SessionSQLStorage) storage).getNumberOfSessions(tenantConfig.tenantIdentifier) > 0;
tenantStat.addProperty("usersCount", usersCount);
tenantStat.addProperty("hasUsersOrSessions", hasUsersOrSessions);

try {
Expand Down Expand Up @@ -284,6 +288,17 @@ public JsonObject getPaidFeatureStats() throws StorageQueryException, TenantOrAp

EE_FEATURES[] features = getEnabledEEFeaturesFromDbOrCache();

if (!Arrays.asList(features).contains(EE_FEATURES.MULTI_TENANCY)) { // Check for multitenancy on the base app
EE_FEATURES[] baseFeatures = FeatureFlag.getInstance(main, new AppIdentifier(null, null))
.getEnabledFeatures();
for (EE_FEATURES feature: baseFeatures) {
if (feature == EE_FEATURES.MULTI_TENANCY) {
features = Arrays.copyOf(features, features.length + 1);
features[features.length - 1] = EE_FEATURES.MULTI_TENANCY;
}
}
}

for (EE_FEATURES feature : features) {
if (feature == EE_FEATURES.DASHBOARD_LOGIN) {
usageStats.add(EE_FEATURES.DASHBOARD_LOGIN.toString(), getDashboardLoginStats());
Expand Down
13 changes: 12 additions & 1 deletion src/main/java/io/supertokens/cronjobs/telemetry/Telemetry.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
import io.supertokens.cronjobs.CronTaskTest;
import io.supertokens.httpRequest.HttpRequest;
import io.supertokens.httpRequest.HttpRequestMocking;
import io.supertokens.pluginInterface.ActiveUsersStorage;
import io.supertokens.pluginInterface.KeyValueInfo;
import io.supertokens.pluginInterface.STORAGE_TYPE;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
Expand Down Expand Up @@ -88,13 +90,22 @@ protected void doTaskPerApp(AppIdentifier app) throws Exception {
json.addProperty("telemetryId", telemetryId.value);
json.addProperty("superTokensVersion", coreVersion);

if (StorageLayer.getBaseStorage(main).getType() == STORAGE_TYPE.SQL) {
ActiveUsersStorage activeUsersStorage = (ActiveUsersStorage) StorageLayer.getStorage(app.getAsPublicTenantIdentifier(), main);
json.addProperty("mau", activeUsersStorage.countUsersActiveSince(app, System.currentTimeMillis() - 30 * 24 * 3600 * 1000L));
} else {
json.addProperty("mau", 0);
}
json.addProperty("appId", app.getAppId());
json.addProperty("connectionUriDomain", app.getConnectionUriDomain());

String url = "https://api.supertokens.io/0/st/telemetry";

// we call the API only if we are not testing the core, of if the request can be mocked (in case a test
// wants
// to use this)
if (!Main.isTesting || HttpRequestMocking.getInstance(main).getMockURL(REQUEST_ID, url) != null) {
HttpRequest.sendJsonPOSTRequest(main, REQUEST_ID, url, json, 10000, 10000, 0);
HttpRequest.sendJsonPOSTRequest(main, REQUEST_ID, url, json, 10000, 10000, 4);
ProcessState.getInstance(main).addState(ProcessState.PROCESS_STATE.SENT_TELEMETRY, null);
}
}
Expand Down
241 changes: 241 additions & 0 deletions src/test/java/io/supertokens/test/FeatureFlagTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -383,15 +383,256 @@ public void testThatMultitenantStatsAreAccurate() throws Exception {
if (tenantId.equals("public")) {
assertFalse(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertFalse(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(0, tenantStatObj.get("usersCount").getAsLong());
} else if (tenantId.equals("t0")) {
assertTrue(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertFalse(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(1, tenantStatObj.get("usersCount").getAsLong());
} else if (tenantId.equals("t1")) {
assertTrue(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertFalse(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(0, tenantStatObj.get("usersCount").getAsLong());
} else if (tenantId.equals("t2")) {
assertFalse(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertTrue(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(0, tenantStatObj.get("usersCount").getAsLong());
}
}

process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}

@Test
public void testThatMultitenantStatsAreAccurateForAnApp() throws Exception {
String[] args = {"../"};

TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));

if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}

FeatureFlag.getInstance(process.main).setLicenseKeyAndSyncFeatures(OPAQUE_KEY_WITH_MULTITENANCY_FEATURE);

Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantIdentifier(null, null, null),
new TenantConfig(
new TenantIdentifier(null, "a1", null),
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
new JsonObject()
)
);

for (int i=0; i<5; i++) {
JsonObject coreConfig = new JsonObject();
StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())
.modifyConfigToAddANewUserPoolForTesting(coreConfig, i+1);

TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", "t" + i);
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantIdentifier(null, "a1", null),
new TenantConfig(
tenantIdentifier,
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
coreConfig
)
);

TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(
StorageLayer.getStorage(tenantIdentifier, process.getProcess()));
if (i % 3 == 0) {
// Create a user
EmailPassword.signUp(
tenantIdentifierWithStorage, process.getProcess(), "[email protected]", "password");
} else if (i % 3 == 1) {
// Create a session
Session.createNewSession(tenantIdentifierWithStorage, process.getProcess(), "userid", new JsonObject(), new JsonObject());
} else {
// Create an enterprise provider
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantIdentifier(null, "a1", null),
new TenantConfig(
tenantIdentifier,
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[]{
new ThirdPartyConfig.Provider("okta", "Okta", null, null, null, null, null, null, null, null, null, null, null, null)
}),
new PasswordlessConfig(true),
coreConfig
)
);
}
}

JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "",
"http://localhost:3567/appid-a1/ee/featureflag",
null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion().get(), "");
Assert.assertEquals("OK", response.get("status").getAsString());

JsonArray multitenancyStats = response.get("usageStats").getAsJsonObject().get("multi_tenancy").getAsJsonObject().get("tenants").getAsJsonArray();
assertEquals(6, multitenancyStats.size());

Set<String> userPoolIds = new HashSet<>();
for (JsonElement tenantStat : multitenancyStats) {
JsonObject tenantStatObj = tenantStat.getAsJsonObject();
String tenantId = tenantStatObj.get("tenantId").getAsString();

if (!StorageLayer.isInMemDb(process.getProcess())) {
// Ensure each userPoolId is unique
String userPoolId = tenantStatObj.get("userPoolId").getAsString();
assertFalse(userPoolIds.contains(userPoolId));
userPoolIds.add(userPoolId);
}

if (tenantId.equals("public")) {
assertFalse(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertFalse(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(0, tenantStatObj.get("usersCount").getAsLong());
} else if (tenantId.equals("t0")) {
assertTrue(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertFalse(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(1, tenantStatObj.get("usersCount").getAsLong());
} else if (tenantId.equals("t1")) {
assertTrue(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertFalse(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(0, tenantStatObj.get("usersCount").getAsLong());
} else if (tenantId.equals("t2")) {
assertFalse(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertTrue(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(0, tenantStatObj.get("usersCount").getAsLong());
}
}

process.kill();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED));
}

@Test
public void testThatMultitenantStatsAreAccurateForACud() throws Exception {
String[] args = {"../"};

TestingProcessManager.TestingProcess process = TestingProcessManager.start(args);
process.startProcess();
assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED));

if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) {
return;
}

if (StorageLayer.isInMemDb(process.getProcess())) {
return;
}

FeatureFlag.getInstance(process.main).setLicenseKeyAndSyncFeatures(OPAQUE_KEY_WITH_MULTITENANCY_FEATURE);

{
JsonObject coreConfig = new JsonObject();
StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())
.modifyConfigToAddANewUserPoolForTesting(coreConfig, 1);
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantIdentifier(null, null, null),
new TenantConfig(
new TenantIdentifier("127.0.0.1", null, null),
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
coreConfig
)
);
}

for (int i=0; i<5; i++) {
JsonObject coreConfig = new JsonObject();
StorageLayer.getStorage(new TenantIdentifier(null, null, null), process.getProcess())
.modifyConfigToAddANewUserPoolForTesting(coreConfig, i+2);

TenantIdentifier tenantIdentifier = new TenantIdentifier("127.0.0.1", null, "t" + i);
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantIdentifier("127.0.0.1", null, null),
new TenantConfig(
tenantIdentifier,
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, null),
new PasswordlessConfig(true),
coreConfig
)
);

TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(
StorageLayer.getStorage(tenantIdentifier, process.getProcess()));
if (i % 3 == 0) {
// Create a user
EmailPassword.signUp(
tenantIdentifierWithStorage, process.getProcess(), "[email protected]", "password");
} else if (i % 3 == 1) {
// Create a session
Session.createNewSession(tenantIdentifierWithStorage, process.getProcess(), "userid", new JsonObject(), new JsonObject());
} else {
// Create an enterprise provider
Multitenancy.addNewOrUpdateAppOrTenant(
process.getProcess(),
new TenantIdentifier("127.0.0.1", null, null),
new TenantConfig(
tenantIdentifier,
new EmailPasswordConfig(true),
new ThirdPartyConfig(true, new ThirdPartyConfig.Provider[]{
new ThirdPartyConfig.Provider("okta", "Okta", null, null, null, null, null, null, null, null, null, null, null, null)
}),
new PasswordlessConfig(true),
coreConfig
)
);
}
}

JsonObject response = HttpRequestForTesting.sendGETRequest(process.getProcess(), "",
"http://127.0.0.1:3567/ee/featureflag",
null, 1000, 1000, null, WebserverAPI.getLatestCDIVersion().get(), "");
Assert.assertEquals("OK", response.get("status").getAsString());

JsonArray multitenancyStats = response.get("usageStats").getAsJsonObject().get("multi_tenancy").getAsJsonObject().get("tenants").getAsJsonArray();
assertEquals(6, multitenancyStats.size());

Set<String> userPoolIds = new HashSet<>();
for (JsonElement tenantStat : multitenancyStats) {
JsonObject tenantStatObj = tenantStat.getAsJsonObject();
String tenantId = tenantStatObj.get("tenantId").getAsString();

if (!StorageLayer.isInMemDb(process.getProcess())) {
// Ensure each userPoolId is unique
String userPoolId = tenantStatObj.get("userPoolId").getAsString();
assertFalse(userPoolIds.contains(userPoolId));
userPoolIds.add(userPoolId);
}

if (tenantId.equals("public")) {
assertFalse(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertFalse(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(0, tenantStatObj.get("usersCount").getAsLong());
} else if (tenantId.equals("t0")) {
assertTrue(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertFalse(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(1, tenantStatObj.get("usersCount").getAsLong());
} else if (tenantId.equals("t1")) {
assertTrue(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertFalse(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(0, tenantStatObj.get("usersCount").getAsLong());
} else if (tenantId.equals("t2")) {
assertFalse(tenantStatObj.get("hasUsersOrSessions").getAsBoolean());
assertTrue(tenantStatObj.get("hasEnterpriseLogin").getAsBoolean());
assertEquals(0, tenantStatObj.get("usersCount").getAsLong());
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/test/java/io/supertokens/test/TelemetryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ protected URLConnection openConnection(URL u) {
assertTrue(telemetryData.has("telemetryId"));
assertEquals(telemetryData.get("superTokensVersion").getAsString(),
Version.getVersion(process.getProcess()).getCoreVersion());
assertEquals(telemetryData.get("appId").getAsString(), "public");
assertEquals(telemetryData.get("connectionUriDomain").getAsString(), "");
assertTrue(telemetryData.has("mau"));

process.kill();
assertNotNull(process.checkOrWaitForEvent(PROCESS_STATE.STOPPED));
Expand Down

0 comments on commit 7358d22

Please sign in to comment.