Created
February 19, 2023 13:25
-
-
Save paustint/e55e5c0ec9411e4dc59935b96d1eb6ac to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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