Skip to content

Commit 7857b35

Browse files
committed
feat(core): init posthog
1 parent 9136699 commit 7857b35

File tree

28 files changed

+394
-135
lines changed

28 files changed

+394
-135
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"pg-protocol": "^1.6.0",
8989
"pkg-dir": "^8.0.0",
9090
"pluralize": "^8.0.0",
91+
"posthog-node": "^5.9.2",
9192
"qrcode": "^1.5.3",
9293
"raw-body": "^3.0.0",
9394
"redis": "^4.6.14",

packages/core/src/libraries/saml-application/saml-applications.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import RequestError from '#src/errors/RequestError/index.js';
1717
import type Queries from '#src/tenants/Queries.js';
1818
import assertThat from '#src/utils/assert-that.js';
1919

20-
import { ensembleSamlApplication, generateKeyPairAndCertificate } from './utils.js';
20+
import { assembleSamlApplication, generateKeyPairAndCertificate } from './utils.js';
2121

2222
const consoleLog = new ConsoleLog(chalk.magenta('SAML app custom domain'));
2323

@@ -75,7 +75,7 @@ export const createSamlApplicationsLibrary = (queries: Queries) => {
7575

7676
const samlConfig = await findSamlApplicationConfigByApplicationId(application.id);
7777

78-
return ensembleSamlApplication({ application, samlConfig });
78+
return assembleSamlApplication({ application, samlConfig });
7979
};
8080

8181
const updateSamlApplicationById = async (
@@ -112,7 +112,7 @@ export const createSamlApplicationsLibrary = (queries: Queries) => {
112112
: originalAppConfig,
113113
]);
114114

115-
return ensembleSamlApplication({
115+
return assembleSamlApplication({
116116
application: updatedApplication,
117117
samlConfig: upToDateSamlConfig,
118118
});

packages/core/src/libraries/saml-application/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export const calculateCertificateFingerprints = (
123123
* - A record from the `applications` table with a `type` of `SAML`
124124
* - A record from the `saml_application_configs` table
125125
*/
126-
export const ensembleSamlApplication = ({
126+
export const assembleSamlApplication = ({
127127
application,
128128
samlConfig,
129129
}: {

packages/core/src/libraries/user.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MfaFactor, UsersPasswordEncryptionMethod } from '@logto/schemas';
1+
import { defaultTenantId, MfaFactor, UsersPasswordEncryptionMethod } from '@logto/schemas';
22
import { createMockUtils } from '@logto/shared/esm';
33

44
import { mockResource, mockAdminUserRole, mockScope } from '#src/__mocks__/index.js';
@@ -48,7 +48,7 @@ const queries = new MockQueries({
4848
});
4949

5050
describe('generateUserId()', () => {
51-
const { generateUserId } = createUserLibrary(queries);
51+
const { generateUserId } = createUserLibrary(defaultTenantId, queries);
5252

5353
afterEach(() => {
5454
hasUserWithId.mockClear();
@@ -98,7 +98,7 @@ describe('encryptUserPassword()', () => {
9898
});
9999

100100
describe('verifyUserPassword()', () => {
101-
const { verifyUserPassword } = createUserLibrary(queries);
101+
const { verifyUserPassword } = createUserLibrary(defaultTenantId, queries);
102102

103103
describe('Argon2i', () => {
104104
it('resolves when password is correct', async () => {
@@ -235,7 +235,7 @@ describe('verifyUserPassword()', () => {
235235
});
236236

237237
describe('findUserScopesForResourceId()', () => {
238-
const { findUserScopesForResourceIndicator } = createUserLibrary(queries);
238+
const { findUserScopesForResourceIndicator } = createUserLibrary(defaultTenantId, queries);
239239

240240
it('returns scopes that the user has access', async () => {
241241
await expect(
@@ -245,7 +245,7 @@ describe('findUserScopesForResourceId()', () => {
245245
});
246246

247247
describe('findUserRoles()', () => {
248-
const { findUserRoles } = createUserLibrary(queries);
248+
const { findUserRoles } = createUserLibrary(defaultTenantId, queries);
249249

250250
it('returns user roles', async () => {
251251
await expect(findUserRoles(mockUser.id)).resolves.toEqual([mockAdminUserRole]);
@@ -254,7 +254,7 @@ describe('findUserRoles()', () => {
254254

255255
describe('addUserMfaVerification()', () => {
256256
const createdAt = new Date().toISOString();
257-
const { addUserMfaVerification } = createUserLibrary(queries);
257+
const { addUserMfaVerification } = createUserLibrary(defaultTenantId, queries);
258258

259259
beforeAll(() => {
260260
jest.useFakeTimers();

packages/core/src/libraries/user.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import type { BindMfa, CreateUser, Scope, User } from '@logto/schemas';
2-
import { RoleType, UsersPasswordEncryptionMethod } from '@logto/schemas';
2+
import {
3+
adminTenantId,
4+
ProductEvent,
5+
RoleType,
6+
UsersPasswordEncryptionMethod,
7+
} from '@logto/schemas';
38
import { generateStandardShortId, generateStandardId } from '@logto/shared';
49
import type { Nullable } from '@silverhand/essentials';
510
import { deduplicateByKey, condArray } from '@silverhand/essentials';
@@ -15,13 +20,15 @@ import assertThat from '#src/utils/assert-that.js';
1520
import { legacyVerify } from '#src/utils/password.js';
1621
import type { OmitAutoSetFields } from '#src/utils/sql.js';
1722

23+
import { captureDeveloperEvent } from '../utils/posthog.js';
24+
1825
import { convertBindMfaToMfaVerification, encryptUserPassword } from './user.utils.js';
1926

2027
export type InsertUserResult = [User];
2128

2229
export type UserLibrary = ReturnType<typeof createUserLibrary>;
2330

24-
export const createUserLibrary = (queries: Queries) => {
31+
export const createUserLibrary = (tenantId: string, queries: Queries) => {
2532
const {
2633
pool,
2734
roles: { findDefaultRoles, findRolesByRoleNames, findRoleByRoleName, findRolesByRoleIds },
@@ -59,19 +66,30 @@ export const createUserLibrary = (queries: Queries) => {
5966
{ retries, factor: 0 } // No need for exponential backoff
6067
);
6168

69+
type InsertUserOptions = {
70+
/** Additional role names to assign to the user upon creation. */
71+
roleNames?: string[];
72+
/**
73+
* Whether the user is created via an interactive flow (e.g. sign up).
74+
* @default false
75+
*/
76+
isInteractive?: boolean;
77+
};
78+
6279
const insertUser = async (
6380
data: OmitAutoSetFields<CreateUser>,
64-
additionalRoleNames: string[]
81+
options?: InsertUserOptions
6582
): Promise<InsertUserResult> => {
66-
const roleNames = [...EnvSet.values.userDefaultRoleNames, ...additionalRoleNames];
83+
const { isInteractive = false } = options ?? {};
84+
const roleNames = [...EnvSet.values.userDefaultRoleNames, ...(options?.roleNames ?? [])];
6785
const [parameterRoles, defaultRoles] = await Promise.all([
6886
findRolesByRoleNames(roleNames),
6987
findDefaultRoles(RoleType.User),
7088
]);
7189

7290
assertThat(parameterRoles.length === roleNames.length, 'role.default_role_missing');
7391

74-
return pool.transaction(async (connection) => {
92+
const result = await pool.transaction<[User]>(async (connection) => {
7593
const user = await insertUserQuery(data);
7694
const roles = deduplicateByKey([...parameterRoles, ...defaultRoles], 'id');
7795

@@ -84,6 +102,12 @@ export const createUserLibrary = (queries: Queries) => {
84102

85103
return [user];
86104
});
105+
106+
if (tenantId === adminTenantId) {
107+
captureDeveloperEvent(result[0].id, ProductEvent.DeveloperCreated, { isInteractive });
108+
}
109+
110+
return result;
87111
};
88112

89113
const checkIdentifierCollision = async (

packages/core/src/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import initI18n from './i18n/init.js';
1212
import SystemContext from './tenants/SystemContext.js';
1313
import { tenantPool } from './tenants/index.js';
1414
import { loadConnectorFactories } from './utils/connectors/index.js';
15+
import { shutdownPostHog } from './utils/posthog.js';
1516

1617
const consoleLog = new ConsoleLog(chalk.magenta('index'));
1718

@@ -54,5 +55,9 @@ try {
5455
consoleLog.error('Error while initializing app:');
5556
consoleLog.error(error);
5657

57-
void Promise.all([trySafe(tenantPool.endAll()), trySafe(redisCache.disconnect())]);
58+
void Promise.all([
59+
trySafe(tenantPool.endAll()),
60+
trySafe(redisCache.disconnect()),
61+
shutdownPostHog(),
62+
]);
5863
}

packages/core/src/routes/admin-user/basics.ts

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/* eslint-disable max-lines */
22
import { emailRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit';
33
import {
4+
ProductEvent,
45
UsersPasswordEncryptionMethod,
6+
adminTenantId,
57
jsonObjectGuard,
68
userProfileGuard,
79
userProfileResponseGuard,
@@ -16,13 +18,14 @@ import koaGuard from '#src/middleware/koa-guard.js';
1618
import assertThat from '#src/utils/assert-that.js';
1719

1820
import { parseLegacyPassword } from '../../utils/password.js';
21+
import { captureDeveloperEvent } from '../../utils/posthog.js';
1922
import { transpileUserProfileResponse } from '../../utils/user.js';
2023
import type { ManagementApiRouter, RouterInitArgs } from '../types.js';
2124

2225
export default function adminUserBasicsRoutes<T extends ManagementApiRouter>(
2326
...args: RouterInitArgs<T>
2427
) {
25-
const [router, { queries, libraries }] = args;
28+
const [router, { queries, libraries, id: tenantId }] = args;
2629
const {
2730
users: {
2831
deleteUserById,
@@ -201,26 +204,23 @@ export default function adminUserBasicsRoutes<T extends ManagementApiRouter>(
201204

202205
const id = await generateUserId();
203206

204-
const [user] = await insertUser(
205-
{
206-
id,
207-
primaryEmail,
208-
primaryPhone,
209-
username,
210-
name,
211-
avatar,
212-
...conditional(customData && { customData }),
213-
...conditional(password && (await encryptUserPassword(password))),
214-
...conditional(
215-
passwordDigest && {
216-
passwordEncrypted: passwordDigest,
217-
passwordEncryptionMethod: passwordAlgorithm,
218-
}
219-
),
220-
...conditional(profile && { profile }),
221-
},
222-
[]
223-
);
207+
const [user] = await insertUser({
208+
id,
209+
primaryEmail,
210+
primaryPhone,
211+
username,
212+
name,
213+
avatar,
214+
...conditional(customData && { customData }),
215+
...conditional(password && (await encryptUserPassword(password))),
216+
...conditional(
217+
passwordDigest && {
218+
passwordEncrypted: passwordDigest,
219+
passwordEncryptionMethod: passwordAlgorithm,
220+
}
221+
),
222+
...conditional(profile && { profile }),
223+
});
224224

225225
ctx.body = transpileUserProfileResponse(user);
226226
return next();
@@ -379,6 +379,9 @@ export default function adminUserBasicsRoutes<T extends ManagementApiRouter>(
379379
await signOutUser(userId);
380380
await deleteUserById(userId);
381381

382+
if (tenantId === adminTenantId) {
383+
captureDeveloperEvent(userId, ProductEvent.DeveloperDeleted);
384+
}
382385
ctx.status = 204;
383386

384387
// Manually trigger the `User.Deleted` hook since we need to send the user data in the payload

packages/core/src/routes/applications/application.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
demoAppApplicationId,
99
hasSecrets,
1010
InternalRole,
11+
ProductEvent,
1112
} from '@logto/schemas';
1213
import { generateStandardId, generateStandardSecret } from '@logto/shared';
1314
import { conditional } from '@silverhand/essentials';
@@ -20,6 +21,7 @@ import { buildOidcClientMetadata } from '#src/oidc/utils.js';
2021
import assertThat from '#src/utils/assert-that.js';
2122
import { parseSearchParamsForSearch } from '#src/utils/search.js';
2223

24+
import { captureEvent } from '../../utils/posthog.js';
2325
import type { ManagementApiRouter, RouterInitArgs } from '../types.js';
2426

2527
import applicationCustomDataRoutes from './application-custom-data.js';
@@ -230,6 +232,10 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
230232
void quota.reportSubscriptionUpdatesUsage('thirdPartyApplicationsLimit');
231233
}
232234

235+
captureEvent(tenantId, ProductEvent.AppCreated, {
236+
type: rest.type,
237+
isThirdParty: rest.isThirdParty ?? false,
238+
});
233239
return next();
234240
}
235241
);
@@ -392,6 +398,11 @@ export default function applicationRoutes<T extends ManagementApiRouter>(
392398
void quota.reportSubscriptionUpdatesUsage('thirdPartyApplicationsLimit');
393399
}
394400

401+
captureEvent(tenantId, ProductEvent.AppDeleted, {
402+
type,
403+
isThirdParty,
404+
});
405+
395406
return next();
396407
}
397408
);

packages/core/src/routes/connector/index.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { type ConnectorFactory } from '@logto/cli/lib/connector/index.js';
22
import type router from '@logto/cloud/routes';
33
import { demoConnectorIds, validateConfig } from '@logto/connector-kit';
4-
import { Connectors, ConnectorType, connectorResponseGuard, type JsonObject } from '@logto/schemas';
4+
import {
5+
Connectors,
6+
ConnectorType,
7+
connectorResponseGuard,
8+
type JsonObject,
9+
ProductEvent,
10+
} from '@logto/schemas';
511
import { generateStandardShortId } from '@logto/shared';
612
import { conditional } from '@silverhand/essentials';
713
import cleanDeep from 'clean-deep';
@@ -16,6 +22,7 @@ import { buildExtraInfo } from '#src/utils/connectors/extra-information.js';
1622
import { loadConnectorFactories, transpileLogtoConnector } from '#src/utils/connectors/index.js';
1723
import { checkSocialConnectorTargetAndPlatformUniqueness } from '#src/utils/connectors/platform.js';
1824

25+
import { captureEvent } from '../../utils/posthog.js';
1926
import type { ManagementApiRouter, RouterInitArgs } from '../types.js';
2027

2128
import connectorAuthorizationUriRoutes from './authorization-uri.js';
@@ -32,6 +39,10 @@ const guardConnectorsQuota = async (
3239
};
3340

3441
const passwordlessConnector = new Set([ConnectorType.Email, ConnectorType.Sms]);
42+
const pickFactoryProperties = <T extends ConnectorFactory<typeof router>>(factory: T) => ({
43+
type: factory.type,
44+
name: factory.metadata.name.en,
45+
});
3546

3647
export default function connectorRoutes<T extends ManagementApiRouter>(
3748
...[router, tenant]: RouterInitArgs<T>
@@ -181,6 +192,18 @@ export default function connectorRoutes<T extends ManagementApiRouter>(
181192
if (conflictingConnectorIds.length > 0) {
182193
await deleteConnectorByIds(conflictingConnectorIds);
183194
}
195+
196+
captureEvent(
197+
tenant.id,
198+
ProductEvent.PasswordlessConnectorUpdated,
199+
pickFactoryProperties(connectorFactory)
200+
);
201+
} else {
202+
captureEvent(
203+
tenant.id,
204+
ProductEvent.SocialConnectorCreated,
205+
pickFactoryProperties(connectorFactory)
206+
);
184207
}
185208

186209
const connector = await getLogtoConnectorById(insertConnectorId);
@@ -347,6 +370,7 @@ export default function connectorRoutes<T extends ManagementApiRouter>(
347370
);
348371

349372
router.delete(
373+
// eslint-disable-next-line max-lines -- refactor later
350374
'/connectors/:id',
351375
koaGuard({ params: object({ id: string().min(1) }), status: [204, 404] }),
352376
async (ctx, next) => {
@@ -365,6 +389,11 @@ export default function connectorRoutes<T extends ManagementApiRouter>(
365389

366390
if (connectorFactory?.type === ConnectorType.Social) {
367391
await removeUnavailableSocialConnectorTargets();
392+
captureEvent(
393+
tenant.id,
394+
ProductEvent.SocialConnectorDeleted,
395+
pickFactoryProperties(connectorFactory)
396+
);
368397
}
369398

370399
ctx.status = 204;
@@ -376,5 +405,4 @@ export default function connectorRoutes<T extends ManagementApiRouter>(
376405
connectorConfigTestingRoutes(router, tenant);
377406
connectorAuthorizationUriRoutes(router, tenant);
378407
connectorFactoryRoutes(router, tenant);
379-
// eslint-disable-next-line max-lines
380408
}

0 commit comments

Comments
 (0)