Skip to content

Instantly share code, notes, and snippets.

@paustint
Created February 19, 2023 13:25
Show Gist options
  • Save paustint/e55e5c0ec9411e4dc59935b96d1eb6ac to your computer and use it in GitHub Desktop.
Save paustint/e55e5c0ec9411e4dc59935b96d1eb6ac to your computer and use it in GitHub Desktop.
// Name: List Jetstream users
import "@johnlindquist/kit";
import { unzipSync } from "zlib";
let papaparse: typeof import("papaparse") = await npm("papaparse");
let { App }: typeof import("@slack/bolt") = await npm("@slack/bolt");
let {
formatISO,
format,
addDays,
isAfter,
parseISO,
startOfDay,
}: typeof import("date-fns") = await npm("date-fns");
let { orderBy }: typeof import("lodash") = await npm("lodash");
/**
* TODO:
* fetch users from auth0
* post as an attachment to slack
*/
const containerClassName = "flex justify-center items-center text-4xl h-full";
const domain = "getjetstream.us.auth0.com";
const clientId = "9rV6bzROJjOJ5cL842Gs3hJeK3njDKI7";
const clientSecret = await env("AUTH_0_SIGNING_SECRET");
const connectionId = "con_EWZrS8B4Igi9kcLE";
const slackSigningToken = await env("SLACK_SIGNING_TOKEN");
const botToken = await env("SLACK_BOT_TOKEN");
interface TokenResponse {
access_token: number;
expires_in: number;
scope: number;
token_type: number;
}
interface UserExportResponse {
id: string;
type: string;
status: string;
connection_id: string;
location?: string;
format: string;
limit: number;
fields: { name: string }[];
connection: string;
created_at: string;
}
interface User {
created_at: string;
email: string;
identities: { connection: string }[];
last_login: string;
logins_count: number;
name: string;
user_id: string;
}
interface UserStats {
total: number;
created7Days: User[];
created30Days: User[];
active7Days: User[];
active30Days: User[];
mostActiveUsers: User[];
}
async function getAccessToken(): Promise<TokenResponse> {
const response = await fetch(`https://${domain}/oauth/token`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: clientId,
client_secret: clientSecret,
audience: "https://getjetstream.us.auth0.com/api/v2/",
}),
});
return await response.json();
}
async function getUsers() {
const { access_token } = await getAccessToken();
let results: UserExportResponse = await (
await fetch(`https://${domain}/api/v2/jobs/users-exports`, {
method: "POST",
headers: {
authorization: `Bearer ${access_token}`,
"content-type": "application/json",
},
body: JSON.stringify({
connection_id: connectionId,
format: "json",
// limit: 5000,
fields: [
{ name: "user_id" },
{ name: "email" },
{ name: "name" },
{ name: "created_at" },
{ name: "identities" },
{ name: "last_login" },
{ name: "logins_count" },
],
}),
})
).json();
while (results.status === "pending") {
await delay(5000);
results = await (
await fetch(`https://${domain}/api/v2/jobs/${results.id}`, {
headers: { authorization: `Bearer ${access_token}` },
method: "GET",
})
).json();
}
if (results.location) {
const users = unzipSync(
await (await fetch(results.location)).arrayBuffer()
);
const usersJson: User[] = users
.toString("utf-8")
.split("\n")
.filter(Boolean)
.map((item) => {
try {
return JSON.parse(item);
} catch (ex) {
return null;
}
})
.filter(Boolean);
const csv = papaparse.unparse(
usersJson.map((user) => ({
...user,
identities: user.identities
.map((identity) => identity.connection)
.join(", "),
})),
{
columns: [
"user_id",
"email",
"name",
"created_at",
"identities",
"last_login",
"logins_count",
],
}
);
await writeFile("tmp/list-auth0-users/users.csv", csv, "utf-8");
return [csv, usersJson] as const;
}
return [];
}
function getUserStats(users: User[]) {
const weekAgo = addDays(startOfDay(new Date()), -7);
const monthAgo = addDays(startOfDay(new Date()), -30);
return users.reduce(
(acc: UserStats, user) => {
const created = parseISO(user.created_at);
const updated = parseISO(user.last_login);
if (isAfter(created, weekAgo)) {
acc.created7Days.push(user);
}
if (isAfter(created, monthAgo)) {
acc.created30Days.push(user);
}
if (isAfter(updated, weekAgo)) {
acc.active7Days.push(user);
}
if (isAfter(updated, monthAgo)) {
acc.active30Days.push(user);
}
return acc;
},
{
total: users.length,
created7Days: [],
created30Days: [],
active7Days: [],
active30Days: [],
mostActiveUsers: orderBy(users, ["logins_count"], ["desc"]).slice(0, 15),
}
);
}
async function postUsersToSlack(csv: string, users: User[]) {
const stats = getUserStats(users);
const app = new App({
signingSecret: slackSigningToken,
token: botToken,
});
const results = await app.client.chat.postMessage({
channel: "C01BA5BQMDW",
text: ":person_in_tuxedo: User Activity Report :person_in_tuxedo:",
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: ":person_in_tuxedo: User Activity Report :person_in_tuxedo:",
},
},
{
type: "context",
elements: [
{
text: `*${format(
new Date(),
"EEEE, MMMM do yyyy"
)}* | Exported from Auth0`,
type: "mrkdwn",
},
],
},
{
type: "divider",
},
{
type: "section",
text: {
type: "mrkdwn",
text: [
`- *Total Users:* ${stats.total}`,
`- *New Users (7 days):* ${stats.created7Days.length}`,
`- *New Users (30 days):* ${stats.created30Days.length}`,
`- *Active Users (7 days):* ${stats.active7Days.length}`,
`- *Active Users (30 days):* ${stats.active30Days.length}`,
].join("\n"),
},
},
],
});
const response = await app.client.files.upload({
channels: "C01BA5BQMDW",
content: csv,
filename: `user-list-${formatISO(new Date(), {
representation: "date",
})}.csv`,
filetype: "csv",
initial_comment: "Current list of users",
title: "Jetstream User List",
thread_ts: results.ts,
});
// curl https://slack.com/api/conversations.list -H "Authorization: Bearer xoxb-1234..."
}
/**
* this function returns a promise that resolves after a given number of milliseconds
*/
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const [csv, usersJson] = await getUsers();
try {
if (!usersJson || !csv) {
throw new Error("Users were not obtained from Auth0.");
}
await postUsersToSlack(csv, usersJson);
const stats = getUserStats(usersJson);
await div(
md(`
Your message has been posted to slack. 🚀
- *Total Users:* ${stats.total}
- *New Users (7 days):* ${stats.created7Days.length}
- *New Users (30 days):* ${stats.created30Days.length}
- *Active Users (7 days):* ${stats.active7Days.length}
- *Active Users (30 days):* ${stats.active30Days.length}
`),
containerClassName
);
} catch (ex) {
await div(
`
🛰️ Error ${ex.message} 😭
`,
containerClassName
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment