npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sf-shield-events

v0.3.0

Published

Extract Salesforce Shield Event Monitoring logs and turn them into Rill dashboards

Downloads

479

Readme

sf-shield-events

CI

An sf CLI plugin that pulls Salesforce Shield Event Monitoring logs out of an org into a directory of Parquet tables you can ask questions of with DuckDB, with the usual questions already written. Optionally, a Rill project over the same tables, or an MCP server so an assistant can ask.

sf shield events discover --target-org my-org
sf shield events extract  --target-org my-org --event-type ApexExecution --event-type Login
cd output && duckdb -init shield.sql      # then, at the DuckDB prompt:
.read queries/slow_apex_entry_points.sql

Two commands and a database: find out what the org has, download it, ask. Two more, both optional, put dashboards over the same tables or serve them to an AI assistant.

Status

Validated end to end against a Shield org, the whole catalogue: 34 event types, 665 log files, 98 MB, streamed into 464,031 rows across 34 typed Parquet tables in 95 seconds at a peak of 363 MB of memory. The tables come to 6 MB. Every one of the six ready-made queries ran, and Rill reconciled the 102 resources of the optional project with no warnings.

Also measured, on a production org: one day of one event type is a 412 MB log file, and the largest seen is 910 MB, which is why nothing here buffers a download (specs/0006).

Why this exists

Shield Event Monitoring answers questions worth asking. Which integration is burning the API limit, which Apex entry point got slower this week, who exported a report at two in the morning. The data is there, in EventLogFile, and getting to it is the problem: one CSV per event type per day, delivered as a blob you have to fetch record by record, with columns that differ by event type and change between API versions.

So most people either buy a product for it, or write the same throwaway script again. This is that script, written once, with the parts that usually rot replaced by inference, and the questions people actually ask kept as SQL files you can read and change (specs/0007).

Installing it

sf plugins install sf-shield-events

The CLI will say it cannot verify the publisher, because the plugin is not signed by Salesforce. That is expected. The plugin carries DuckDB as a native dependency, about 115 MB once installed; behind a corporate proxy the install honours npm's proxy settings (HTTPS_PROXY, or npm config set proxy) like any other plugin.

Requires Node 22.6 or later, which the sf CLI already bundles. DuckDB comes with the plugin, so the tables are written without installing anything else; to ask questions of them you want the DuckDB CLI, any version. Only the optional dashboards need Rill.

What the commands do

sf shield events discover

Asks the org which event types it has logs for, and for each one how many files, how large, and which days they cover. No list of event types is built into this tool: the answer depends on the edition, on which Shield features are licensed, and on what happened in the org inside the retention window.

An org with Shield or the Event Monitoring add-on generates event log files by default and keeps them for thirty days. A Developer Edition or trial org has to opt in first, under Setup > Event Monitoring Settings > Generate event log files, and keeps one day; without the opt-in the org has no files at all, which discover reports as none rather than as an error.

sf shield events extract

Downloads the log files for the event types you name and writes one Parquet table per type, then makes the directory a database. Name no types and, in a terminal, it asks instead: a list of the org's event types with the files, size and days each one costs, then the first and last day, proposed as what you picked covers. Before downloading it prints the command line that runs the same extraction without a question, so the second time can be a script (specs/0009). Outside a terminal, with --json or with --no-prompt, --event-type is required, and a pipeline never sees a prompt.

Either way it makes the directory a database: shield.sql opens every table as a view, and queries/ holds one SQL file per question people bring to event logs.

The columns of a table are the union of what the org declares across the matching files, not the columns of the first one. Salesforce changes them between API versions, and an extraction spanning that change should not lose the difference.

Files are streamed rather than buffered, because they are large: in a busy production org they average 57 MB and the largest measured was 910 MB. The command reports the total download before it starts and asks before anything over 500 MB, since a single day of one busy event type can mean half a gigabyte (specs/0006). A download that fails is retried, an expired session is refreshed, and a retry never doubles a file's rows. Each table is then typed and converted by DuckDB under a fixed memory limit, and the conversion is checked row for row: a table that lost a row fails the command rather than reporting success.

Types are given only where nothing is lost: whole numbers, dates, timestamps, booleans. A packed Salesforce timestamp, a version number or a key prefix stays text exactly as the org wrote it (specs/0007).

The flags: --event-type (-e), repeated for each type, or left out to choose from a list; --start-date and --end-date as YYYY-MM-DD, both inclusive; --output-dir, output by default; --no-prompt to skip the question above 500 MB; --interval, Daily by default or Hourly, in an org that keeps both, because reading both would count every event twice; --concurrency, how many event types download at once, four by default, with the files of one type always arriving in order. A type that fails is reported at the end, after the others have been written, and --json returns the tables written as data.

sf shield events extract -o my-org -e ApexExecution --start-date 2026-09-01 --end-date 2026-09-07
cd output
duckdb -init shield.sql
.read queries/slow_apex_entry_points.sql

The six questions shipped, one file each under queries/:

| File | Question | Needs | | ------------------------------ | ------------------------------------------------------------- | ---------------- | | api_usage_by_client.sql | Which client is burning the API limit, per day | ApiTotalUsage | | slow_apex_entry_points.sql | Which Apex entry point got slower this week, against the last | ApexExecution | | report_runs_out_of_hours.sql | Who ran a report outside working hours, and how many rows | AsyncReportRun | | login_failures.sql | Failed logins by user and source address | Login | | dml_volume_by_user.sql | Who wrote the most rows, per day, by operation and object | DatabaseSave | | rest_api_errors.sql | REST calls that failed, by resource, status and client | RestApi |

The queries are opinionated on purpose and name real columns, which nothing else in the tool does. Read queries/README.md, edit them, add your own: a later extraction adds queries it ships that are missing and leaves the ones already there alone. A query over an event type the org lacks fails with "table not found", which is the correct answer.

sf shield events rill (optional)

Reads the tables and writes a Rill project over them: a model, a metrics view and a dashboard per event type. Then rill start output. The tables are already a database without this; Rill adds a point-and-click explorer for the reader who would rather not type SQL. --input-dir names the directory, output by default.

What each column is for comes from the data, not from its name. A column of numbers gets a total and an average; a column of ISO timestamps can be the time axis; a column with few distinct values groups rows; a column with a distinct value per row gets counted instead. An event type this tool has never seen still produces a usable dashboard, which is the whole point (specs/0002).

The project is written beside the tables so rill start output works and the directory can be moved as one thing.

sf shield events mcp (optional)

Serves the tables to a Model Context Protocol client over stdio, so an assistant can list the event types, learn what each column is for, run the ready-made questions and ask its own. The server reads a directory that extract wrote and never talks to the org.

Setting it up

  1. Install the plugin and extract something, as above. The server needs a directory with at least one Parquet table in it; sf shield events mcp on an empty directory exits with "No Parquet tables".

  2. Check that the server starts from a shell. It waits for a client on stdin, so end it with Ctrl+C:

    sf shield events mcp --input-dir /path/to/output

    The one line it prints goes to stderr, and says how many tables it is serving.

  3. Add it to the client as a stdio server. The directory should be an absolute path, because the client decides the working directory, not you.

    Claude Code, in the project you are working in:

    claude mcp add shield-events -- sf shield events mcp --input-dir /path/to/output

    Claude Desktop, in claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/, Windows: %APPDATA%\Claude\):

    {
      "mcpServers": {
        "shield-events": {
          "command": "sf",
          "args": ["shield", "events", "mcp", "--input-dir", "/path/to/output"]
        }
      }
    }

    Any other MCP client takes the same three things: the command sf, the arguments, and the name you give the server. If the client cannot find sf, give it the full path from which sf.

  4. Restart the client, or reload its servers, and ask it what event types the extraction has.

Using it

The server offers five tools, and its instructions tell the assistant to start with the first and the third:

| Tool | What it answers | | ---------------- | -------------------------------------------------------------------------- | | list_tables | The event types in the extraction, each with its row count | | describe_table | One table's columns: DuckDB type and role, with no values shown | | list_questions | The ready-made questions in queries/, each with the event types it needs | | run_question | One of those questions, by name | | query | One SELECT in DuckDB SQL over the tables |

A role is what the column is for, inferred from its values: a dimension groups rows, an identifier is distinct per row and worth counting rather than grouping, a numeric column is worth summing, a timestamp can be a time axis. Things worth asking, in words:

  • "Which event types do we have, and how many rows each?"
  • "Run the login failures question."
  • "Which client burned the most API calls last week, by day?"
  • "In ApexExecution, which entry points had the highest average CPU time?"
  • "Describe the AsyncReportRun table and tell me which columns group the rows."

Every answer is capped at --row-limit rows, 200 by default, and says when it was cut. An assistant that needs more should aggregate or page with OFFSET; the cap is a guard on volume, not a limit on what can be asked. Times are TIMESTAMP_DERIVED, written by Salesforce in UTC.

What it will not do

The server is read-only in the strongest sense DuckDB offers. Only a single SELECT runs, and that is decided by DuckDB's own parser, so COPY, CREATE, INSTALL and SET are refused whatever they look like. DuckDB is confined to the extraction directory and locked there, so a SELECT over any other file on the machine fails with a permission error. describe_table shows no values.

The rows are still unredacted production logs, and the client is usually a model running elsewhere. Point this at an extraction you would be willing to paste into that model (specs/0008).

If it does not work

  • The client says the server failed to start. Run the command from step 2 in a shell. A wrong --input-dir or a missing sf on the client's PATH shows up there.
  • The assistant sees no tools. The directory has no Parquet files. Run extract first, or point --input-dir at where its output went.
  • A question fails with "table not found". The extraction does not have that event type. That is the correct answer; list_questions says what each one needs.

What it does not do

It does not store credentials. The plugin borrows the connection the sf CLI already has, so there is no token, no instance URL and no .env anywhere in it.

It does not filter or redact. What comes out of the org goes into the files, and event logs carry user ids, URIs, query text and IP addresses. Treat the output directory the way you treat the org: .gitignore covers it here, and deleting it when the analysis is done is the habit worth having.

It does not do Real-Time Event Monitoring, the streaming platform events. This reads EventLogFile, the batch side.

Working on it

npm install
npm run check    # lint, typecheck and tests
npm run build    # both bin entrypoints load lib/, so build before running
./bin/run.js shield events discover -o my-org

Tests run on the Node test runner against TypeScript directly, so there is no test framework and no build step for them. The table tests run a real DuckDB on files a few lines long, and one test hands a generated project to rill validate when Rill is installed.

docs/ARCHITECTURE.md describes how the pieces fit. specs/ records why each decision was made, with the measurements behind it.

License

Apache License 2.0. See LICENSE and NOTICE, and specs/0005 for why this and not something restrictive.

Copyright 2026 Adilson Arcoverde.