From 590a411b3f2e45d74d8349c2f04257332f7db8c7 Mon Sep 17 00:00:00 2001 From: Matthew Mauer Date: Thu, 12 Feb 2026 14:59:13 -0500 Subject: [PATCH 1/4] fetching user insights reports --- src/api.ts | 67 +++++++++++++++++++++++++++++ src/api/reports.ts | 75 +++++++++++++++++++++++++++++++++ src/index.ts | 14 +++++++ src/reports.ts | 102 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 src/api/reports.ts create mode 100644 src/reports.ts diff --git a/src/api.ts b/src/api.ts index 511d6e4..13035b8 100644 --- a/src/api.ts +++ b/src/api.ts @@ -130,6 +130,8 @@ import { UserMetadata, } from "./user" import { validateOrgApiKey, validatePersonalApiKey } from "./validators" +import { AttritionReportInterval, ChampionReportInterval, ChurnReportInterval, GrowthReportInterval, OrgReport, OrgReportType, ReengagementReportInterval, ReportPagination, TopInviterReportInterval, UserReport, UserReportType } from "./reports" +import { fetchOrgReport, fetchUserReport } from "./api/reports" export function getApis(authUrl: URL, integrationApiKey: string) { function fetchTokenVerificationMetadataWrapper(): Promise { @@ -462,6 +464,62 @@ export function getApis(authUrl: URL, integrationApiKey: string) { return verifySmsChallenge(authUrl, integrationApiKey, verifySmsChallengeRequest) } + function fetchUserTopInviterReportWrapper( + reportInterval?: TopInviterReportInterval, + pagination?: ReportPagination, + ): Promise { + return fetchUserReport(authUrl, integrationApiKey, UserReportType.TOP_INVITERS, reportInterval, pagination) + } + + function fetchUserChampionReportWrapper( + reportInterval?: ChampionReportInterval, + pagination?: ReportPagination, + ): Promise { + return fetchUserReport(authUrl, integrationApiKey, UserReportType.CHAMPION, reportInterval, pagination) + } + + function fetchUserReengagementReportWrapper( + reportInterval?: ReengagementReportInterval, + pagination?: ReportPagination, + ): Promise { + return fetchUserReport(authUrl, integrationApiKey, UserReportType.REENGAGEMENT, reportInterval, pagination) + } + + function fetchUserChurnReportWrapper( + reportInterval?: ChurnReportInterval, + pagination?: ReportPagination, + ): Promise { + return fetchUserReport(authUrl, integrationApiKey, UserReportType.CHURN, reportInterval, pagination) + } + + function fetchOrgReengagementReportWrapper( + reportInterval?: ReengagementReportInterval, + pagination?: ReportPagination, + ): Promise { + return fetchOrgReport(authUrl, integrationApiKey, OrgReportType.REENGAGEMENT, reportInterval, pagination) + } + + function fetchOrgChurnReportWrapper( + reportInterval?: ChurnReportInterval, + pagination?: ReportPagination, + ): Promise { + return fetchOrgReport(authUrl, integrationApiKey, OrgReportType.CHURN, reportInterval, pagination) + } + + function fetchOrgGrowthReportWrapper( + reportInterval?: GrowthReportInterval, + pagination?: ReportPagination, + ): Promise { + return fetchOrgReport(authUrl, integrationApiKey, OrgReportType.GROWTH, reportInterval, pagination) + } + + function fetchOrgAttritionReportWrapper( + reportInterval?: AttritionReportInterval, + pagination?: ReportPagination, + ): Promise { + return fetchOrgReport(authUrl, integrationApiKey, OrgReportType.ATTRITION, reportInterval, pagination) + } + return { // fetching functions fetchTokenVerificationMetadata: fetchTokenVerificationMetadataWrapper, @@ -536,5 +594,14 @@ export function getApis(authUrl: URL, integrationApiKey: string) { verifySmsChallenge: verifySmsChallengeWrapper, // employee functions fetchEmployeeById: fetchEmployeeByIdWrapper, + // report data fetching functions + fetchUserTopInviterReport: fetchUserTopInviterReportWrapper, + fetchUserChampionReport: fetchUserChampionReportWrapper, + fetchUserReengagementReport: fetchUserReengagementReportWrapper, + fetchUserChurnReport: fetchUserChurnReportWrapper, + fetchOrgReengagementReport: fetchOrgReengagementReportWrapper, + fetchOrgChurnReport: fetchOrgChurnReportWrapper, + fetchOrgGrowthReport: fetchOrgGrowthReportWrapper, + fetchOrgAttritionReport: fetchOrgAttritionReportWrapper, } } diff --git a/src/api/reports.ts b/src/api/reports.ts new file mode 100644 index 0000000..a361505 --- /dev/null +++ b/src/api/reports.ts @@ -0,0 +1,75 @@ +import { + ApiKeyCreateException, + ApiKeyDeleteException, + ApiKeyFetchException, + ApiKeyUpdateException, + ApiKeyValidateException, + ApiKeyValidateRateLimitedException, + RateLimitedException, + ApiKeyImportException +} from "../exceptions" +import { httpRequest } from "../http" +import { OrgReport, OrgReportType, ReportPagination, UserReport, UserReportType } from "../reports" +import { ApiKeyFull, ApiKeyNew, ApiKeyResultPage, ApiKeyValidation } from "../user" +import { formatQueryParameters, isValidHex, parseSnakeCaseToCamelCase, removeBearerIfExists } from "../utils" + +const USER_REPORTS_PATH = "/api/backend/v1/user_report" +const ORG_REPORTS_PATH = "/api/backend/v1/org_report" + +// GET +export function fetchUserReport( + authUrl: URL, + integrationApiKey: string, + reportType: UserReportType, + reportInterval?: string, + pagination?: ReportPagination, +): Promise { + const request = { + report_interval: reportInterval, + page_size: pagination?.pageSize, + page_number: pagination?.pageNumber, + } + const queryString = formatQueryParameters(request) + + return httpRequest(authUrl, integrationApiKey, `${USER_REPORTS_PATH}/${reportType}?${queryString}`, "GET").then((httpResponse) => { + if (httpResponse.statusCode === 401) { + throw new Error("integrationApiKey is incorrect") + } else if (httpResponse.statusCode === 429) { + throw new RateLimitedException(httpResponse.response) + } else if (httpResponse.statusCode === 400) { + throw new ApiKeyFetchException(httpResponse.response) + } else if (httpResponse.statusCode && httpResponse.statusCode >= 400) { + throw new Error("Unknown error when creating the end user api key") + } + + return parseSnakeCaseToCamelCase(httpResponse.response) + }) +} +export function fetchOrgReport( + authUrl: URL, + integrationApiKey: string, + reportType: OrgReportType, + reportInterval?: string, + pagination?: ReportPagination, +): Promise { + const request = { + report_interval: reportInterval, + page_size: pagination?.pageSize, + page_number: pagination?.pageNumber, + } + const queryString = formatQueryParameters(request) + + return httpRequest(authUrl, integrationApiKey, `${ORG_REPORTS_PATH}/${reportType}?${queryString}`, "GET").then((httpResponse) => { + if (httpResponse.statusCode === 401) { + throw new Error("integrationApiKey is incorrect") + } else if (httpResponse.statusCode === 429) { + throw new RateLimitedException(httpResponse.response) + } else if (httpResponse.statusCode === 400) { + throw new ApiKeyFetchException(httpResponse.response) + } else if (httpResponse.statusCode && httpResponse.statusCode >= 400) { + throw new Error("Unknown error when creating the end user api key") + } + + return parseSnakeCaseToCamelCase(httpResponse.response) + }) +} diff --git a/src/index.ts b/src/index.ts index 4f43c66..3a617c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,6 +79,20 @@ export { } from "./exceptions" export type { SocialLoginProvider, SamlLoginProvider, LoginMethod } from "./loginMethod" export type { CustomRoleMappings, CustomRoleMapping } from "./customRoleMappings" +export type { + ReengagementReportInterval, + ChampionReportInterval, + ChurnReportInterval, + GrowthReportInterval, + AttritionReportInterval, + TopInviterReportInterval, + ReportPagination, + UserReport, + UserReportRecord, + OrgReport, + OrgReportRecord, + UserOrgMembershipForReport, +} from './reports' export type { UserProperties, User, diff --git a/src/reports.ts b/src/reports.ts new file mode 100644 index 0000000..2e18e50 --- /dev/null +++ b/src/reports.ts @@ -0,0 +1,102 @@ +export type ReportPagination = { + pageSize?: number, + pageNumber?: number, +} + +// org report types + +export type OrgReportRecord = { + recordId: string, + reportId: string, + orgId: string, + name: string, + numUsers: number, + orgCreatedAt: number, + extraProperties: { [key: string]: any }, +} + +export type OrgReport = { + orgReports: OrgReportRecord[], + currentPage: number, + totalCount: number, + pageSize: number, + hasMoreResults: boolean, + reportTime: number, +} + +export enum OrgReportType { + ATTRITION = "attrition", + REENGAGEMENT = "reengagement", + GROWTH = "growth", + CHURN = "churn", +} + +// user report types + +export type UserOrgMembershipForReport = { + displayName: string, + orgId: string, + userRole: string, +} + +export type UserReportRecord = { + recordId: string, + reportId: string, + userId: string, + email: string, + userCreatedAt: number, + lastActiveAt: number, + username?: string, + firstName?: string, + lastName?: string, + orgData?: UserOrgMembershipForReport[], + extraProperties: { [key: string]: any }, +} + +export type UserReport = { + userReports: UserReportRecord[], + currentPage: number, + totalCount: number, + pageSize: number, + hasMoreResults: boolean, + reportTime: number, +} + +export enum UserReportType { + REENGAGEMENT = "reengagement", + CHURN = "churn", + TOP_INVITERS = "top_inviter", + CHAMPION = "champion", +} + +// report interval options + +export enum ReengagementReportInterval { + WEEKLY = "Weekly", + MONTHLY = "Monthly", +} +export enum ChurnReportInterval { + SEVEN_DAYS = "7", + FOURTEEN_DAYS = "14", + THIRTY_DAYS = "30", +} +export enum GrowthReportInterval { + THIRTY_DAYS = "30", + SIXTY_DAYS = "60", + NINETY_DAYS = "90", +} +export enum TopInviterReportInterval { + THIRTY_DAYS = "30", + SIXTY_DAYS = "60", + NINETY_DAYS = "90", +} +export enum ChampionReportInterval { + THIRTY_DAYS = "30", + SIXTY_DAYS = "60", + NINETY_DAYS = "90", +} +export enum AttritionReportInterval { + THIRTY_DAYS = "30", + SIXTY_DAYS = "60", + NINETY_DAYS = "90", +} \ No newline at end of file From 7dc980a27f8d6e51a15475e8ca512616dfadcaa4 Mon Sep 17 00:00:00 2001 From: Matthew Mauer Date: Fri, 13 Feb 2026 12:45:14 -0500 Subject: [PATCH 2/4] fix record.id name --- src/reports.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reports.ts b/src/reports.ts index 2e18e50..228431a 100644 --- a/src/reports.ts +++ b/src/reports.ts @@ -6,7 +6,7 @@ export type ReportPagination = { // org report types export type OrgReportRecord = { - recordId: string, + id: string, reportId: string, orgId: string, name: string, @@ -40,7 +40,7 @@ export type UserOrgMembershipForReport = { } export type UserReportRecord = { - recordId: string, + id: string, reportId: string, userId: string, email: string, From 1ae2111b37e5168fe99d84c20c3c2a2ad146c965 Mon Sep 17 00:00:00 2001 From: Matthew Mauer Date: Wed, 11 Mar 2026 19:03:30 -0400 Subject: [PATCH 3/4] support fetching chart data for user insights --- src/api.ts | 29 +++++++++++++++++++-- src/api/reports.ts | 63 ++++++++++++++++++++++++++++++++-------------- src/index.ts | 4 +++ src/reports.ts | 27 ++++++++++++++++++++ 4 files changed, 102 insertions(+), 21 deletions(-) diff --git a/src/api.ts b/src/api.ts index 13035b8..24c8f91 100644 --- a/src/api.ts +++ b/src/api.ts @@ -130,8 +130,23 @@ import { UserMetadata, } from "./user" import { validateOrgApiKey, validatePersonalApiKey } from "./validators" -import { AttritionReportInterval, ChampionReportInterval, ChurnReportInterval, GrowthReportInterval, OrgReport, OrgReportType, ReengagementReportInterval, ReportPagination, TopInviterReportInterval, UserReport, UserReportType } from "./reports" -import { fetchOrgReport, fetchUserReport } from "./api/reports" +import { + AttritionReportInterval, + ChampionReportInterval, + ChartData, + ChartMetric, + ChartMetricCadence, + ChurnReportInterval, + GrowthReportInterval, + OrgReport, + OrgReportType, + ReengagementReportInterval, + ReportPagination, + TopInviterReportInterval, + UserReport, + UserReportType +} from "./reports" +import { fetchChartMetricData, fetchOrgReport, fetchUserReport } from "./api/reports" export function getApis(authUrl: URL, integrationApiKey: string) { function fetchTokenVerificationMetadataWrapper(): Promise { @@ -520,6 +535,15 @@ export function getApis(authUrl: URL, integrationApiKey: string) { return fetchOrgReport(authUrl, integrationApiKey, OrgReportType.ATTRITION, reportInterval, pagination) } + function fetchChartMetricDataWrapper( + chartMetric: ChartMetric, + cadence: ChartMetricCadence, + startDate?: Date, + endDate?: Date, + ): Promise { + return fetchChartMetricData(authUrl, integrationApiKey, chartMetric, cadence, startDate, endDate) + } + return { // fetching functions fetchTokenVerificationMetadata: fetchTokenVerificationMetadataWrapper, @@ -603,5 +627,6 @@ export function getApis(authUrl: URL, integrationApiKey: string) { fetchOrgChurnReport: fetchOrgChurnReportWrapper, fetchOrgGrowthReport: fetchOrgGrowthReportWrapper, fetchOrgAttritionReport: fetchOrgAttritionReportWrapper, + fetchChartMetricData: fetchChartMetricDataWrapper, } } diff --git a/src/api/reports.ts b/src/api/reports.ts index a361505..6ce2c2e 100644 --- a/src/api/reports.ts +++ b/src/api/reports.ts @@ -1,20 +1,20 @@ -import { - ApiKeyCreateException, - ApiKeyDeleteException, - ApiKeyFetchException, - ApiKeyUpdateException, - ApiKeyValidateException, - ApiKeyValidateRateLimitedException, - RateLimitedException, - ApiKeyImportException -} from "../exceptions" +import { RateLimitedException } from "../exceptions" import { httpRequest } from "../http" -import { OrgReport, OrgReportType, ReportPagination, UserReport, UserReportType } from "../reports" -import { ApiKeyFull, ApiKeyNew, ApiKeyResultPage, ApiKeyValidation } from "../user" -import { formatQueryParameters, isValidHex, parseSnakeCaseToCamelCase, removeBearerIfExists } from "../utils" +import { + ChartData, + ChartMetric, + ChartMetricCadence, + OrgReport, + OrgReportType, + ReportPagination, + UserReport, + UserReportType, +} from "../reports" +import { formatQueryParameters, parseSnakeCaseToCamelCase, } from "../utils" const USER_REPORTS_PATH = "/api/backend/v1/user_report" const ORG_REPORTS_PATH = "/api/backend/v1/org_report" +const CHART_METRICS_PATH = "/api/backend/v1/chart_metrics" // GET export function fetchUserReport( @@ -36,15 +36,14 @@ export function fetchUserReport( throw new Error("integrationApiKey is incorrect") } else if (httpResponse.statusCode === 429) { throw new RateLimitedException(httpResponse.response) - } else if (httpResponse.statusCode === 400) { - throw new ApiKeyFetchException(httpResponse.response) } else if (httpResponse.statusCode && httpResponse.statusCode >= 400) { - throw new Error("Unknown error when creating the end user api key") + throw new Error("Unknown error when fetching the user report") } return parseSnakeCaseToCamelCase(httpResponse.response) }) } + export function fetchOrgReport( authUrl: URL, integrationApiKey: string, @@ -64,10 +63,36 @@ export function fetchOrgReport( throw new Error("integrationApiKey is incorrect") } else if (httpResponse.statusCode === 429) { throw new RateLimitedException(httpResponse.response) - } else if (httpResponse.statusCode === 400) { - throw new ApiKeyFetchException(httpResponse.response) } else if (httpResponse.statusCode && httpResponse.statusCode >= 400) { - throw new Error("Unknown error when creating the end user api key") + throw new Error("Unknown error when fetching the org report") + } + + return parseSnakeCaseToCamelCase(httpResponse.response) + }) +} + +export function fetchChartMetricData( + authUrl: URL, + integrationApiKey: string, + chartMetric: ChartMetric, + cadence?: ChartMetricCadence, + startDate?: Date, + endDate?: Date, +): Promise { + const request = { + cadence: cadence, + start_date: startDate?.toISOString().slice(0, 10), // format to YYYY-MM-DD + end_date: endDate?.toISOString().slice(0, 10), // format to YYYY-MM-DD + } + const queryString = formatQueryParameters(request) + + return httpRequest(authUrl, integrationApiKey, `${CHART_METRICS_PATH}/${chartMetric}?${queryString}`, "GET").then((httpResponse) => { + if (httpResponse.statusCode === 401) { + throw new Error("integrationApiKey is incorrect") + } else if (httpResponse.statusCode === 429) { + throw new RateLimitedException(httpResponse.response) + } else if (httpResponse.statusCode && httpResponse.statusCode >= 400) { + throw new Error("Unknown error when fetching the chart metric data") } return parseSnakeCaseToCamelCase(httpResponse.response) diff --git a/src/index.ts b/src/index.ts index 3a617c9..f113fe3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -92,6 +92,10 @@ export type { OrgReport, OrgReportRecord, UserOrgMembershipForReport, + ChartData, + ChartDataPoint, + ChartMetric, + ChartMetricCadence, } from './reports' export type { UserProperties, diff --git a/src/reports.ts b/src/reports.ts index 228431a..90a3565 100644 --- a/src/reports.ts +++ b/src/reports.ts @@ -99,4 +99,31 @@ export enum AttritionReportInterval { THIRTY_DAYS = "30", SIXTY_DAYS = "60", NINETY_DAYS = "90", +} + +// chart data types + +export enum ChartMetric { + SIGNUPS = "signups", + ORGS_CREATED = "orgs_created", + ACTIVE_USERS = "active_users", + ACTIVE_ORGS = "active_orgs", +} + +export enum ChartMetricCadence { + DAILY = "Daily", + WEEKLY = "Weekly", + MONTHLY = "Monthly", +} + +export type ChartDataPoint = { + result: number, + date: string, // YYYY-MM-DD format date + cadenceCompleted: boolean, +} + +export type ChartData = { + chartType: string, + cadence: ChartMetricCadence, + metrics: ChartDataPoint[], } \ No newline at end of file From 0d09400e2be3de4240d16eb25de7253d7078f108 Mon Sep 17 00:00:00 2001 From: Matthew Mauer Date: Wed, 11 Mar 2026 19:31:30 -0400 Subject: [PATCH 4/4] rename for clarity --- src/api.ts | 4 ++-- src/api/{reports.ts => userInsights.ts} | 2 +- src/index.ts | 2 +- src/{reports.ts => userInsights.ts} | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename src/api/{reports.ts => userInsights.ts} (99%) rename src/{reports.ts => userInsights.ts} (100%) diff --git a/src/api.ts b/src/api.ts index 24c8f91..ed6c973 100644 --- a/src/api.ts +++ b/src/api.ts @@ -145,8 +145,8 @@ import { TopInviterReportInterval, UserReport, UserReportType -} from "./reports" -import { fetchChartMetricData, fetchOrgReport, fetchUserReport } from "./api/reports" +} from "./userInsights" +import { fetchChartMetricData, fetchOrgReport, fetchUserReport } from "./api/userInsights" export function getApis(authUrl: URL, integrationApiKey: string) { function fetchTokenVerificationMetadataWrapper(): Promise { diff --git a/src/api/reports.ts b/src/api/userInsights.ts similarity index 99% rename from src/api/reports.ts rename to src/api/userInsights.ts index 6ce2c2e..4854d63 100644 --- a/src/api/reports.ts +++ b/src/api/userInsights.ts @@ -9,7 +9,7 @@ import { ReportPagination, UserReport, UserReportType, -} from "../reports" +} from "../userInsights" import { formatQueryParameters, parseSnakeCaseToCamelCase, } from "../utils" const USER_REPORTS_PATH = "/api/backend/v1/user_report" diff --git a/src/index.ts b/src/index.ts index f113fe3..ce24293 100644 --- a/src/index.ts +++ b/src/index.ts @@ -96,7 +96,7 @@ export type { ChartDataPoint, ChartMetric, ChartMetricCadence, -} from './reports' +} from './userInsights' export type { UserProperties, User, diff --git a/src/reports.ts b/src/userInsights.ts similarity index 100% rename from src/reports.ts rename to src/userInsights.ts