Loading

Marketing Cloud Engagement - Audit Installed Packages for last successful login date

Publiseringsdato: Jul 18, 2026
Beskrivelse

App Users are automatically generated when installing packages from the AppExchange or creating API integrations. Over time, these users can accumulate. This article provides a Server-Side JavaScript (SSJS) solution to automate the auditing of active App Users in Marketing Cloud.

The script dynamically evaluates API responses to exclude inactive users, GUID-based usernames, and system-generated roles. It also automatically creates the required Data Extension if it does not exist and clears previous data upon each run to provide a fresh, accurate audit.

Note: Internal Apps (not visibile in setup), or MobilePush Apps (Mobile Push Admin)  may appear in the results and are managed strictly by the system and do not require auditing or manual intervention. Additionally, if an installed package in Setup does not have an Expiration Date (displays as "N/A"), it may not generate a corresponding App User.

Installed packages that had a name updated will not reflect in the app user name, use applicationid to match these packages

The script automatically checks for a Data Extension with the CustomerKey AppUser_LoginAudit. If it is not found, the script generates it automatically using the exact schema below. 

  1. In Marketing Cloud, navigate to Journey Builder > Automation Studio.

  2. Click the Activities tab, then click Create Activity.

  3. Select Script and click Next.

  4. Enter a name for the activity (e.g., Audit App Users SSJS) and click Next.

  5. Paste the following SSJS code into the editor.

    <script runat="server">
    Platform.Load("Core", "1.1.1");
    
    try {
        var targetDEKey = "AppUser_LoginAudit"; 
        var targetDEName = "AppUser_LoginAudit"; 
        var prox = new Script.Util.WSProxy();
        
        // --- DEBUG TOGGLE ---
        // Set to true: Writes ALL API users to the DE, bypassing all exclusion rules
        // Set to false: Normal operation (filters out inactive, GUIDs, and System-created apps)
        var debugMode = false; 
    
        // --- 1. CHECK, CLEAR, OR CREATE DATA EXTENSION ---
        var deRetrieve = prox.retrieve("DataExtension", ["CustomerKey"], {
            Property: "CustomerKey",
            SimpleOperator: "equals",
            Value: targetDEKey
        });
    
        if (deRetrieve && deRetrieve.Results && deRetrieve.Results.length > 0) {
            var clearAction = prox.performItem("DataExtension", { CustomerKey: targetDEKey }, "ClearData");
        } else {
            var newDE = {
                CustomerKey: targetDEKey,
                Name: targetDEName,
                IsSendable: false,
                Fields: [
                    { Name: "UserID", FieldType: "Text", MaxLength: 100, IsPrimaryKey: true, IsRequired: true },
                    { Name: "UserName", FieldType: "Text", MaxLength: 254 },
                    { Name: "LastLogin", FieldType: "Text", MaxLength: 100 },
                    { Name: "ApplicationID", FieldType: "Text", MaxLength: 100 },
                    { Name: "CreatedBy", FieldType: "Text", MaxLength: 254 }, 
                    { Name: "DefaultBusinessUnit", FieldType: "Text", MaxLength: 50 },
                    { Name: "AuditDate", FieldType: "Date", IsRequired: true, DefaultValue: "getdate()" } 
                ]
            };
            var createRes = prox.createItem("DataExtension", newDE);
        }
        
        // --- 2. DEFINE RETRIEVE PARAMETERS ---
        var cols = ["ID", "Name", "LastSuccessfulLogin", "CustomerKey", "Roles", "DefaultBusinessUnit", "ActiveFlag", "IsAPIUser", "UserID"];
        
        // Filter to look for API Users
        var filter = {
            Property: "IsAPIUser",
            SimpleOperator: "equals",
            Value: "true"
        };
        
        var moreData = true;
        var reqID = null;
    
        var guidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
        
        // Cache to store User IDs and their resolved UserNames to minimize API calls
        var userCache = {}; 
    
        // --- 3. RETRIEVE AND PROCESS ACCOUNT USERS ---
        while (moreData) {
            moreData = false;
            
            var res = reqID == null ? prox.retrieve("AccountUser", cols, filter) : prox.getNextBatch("AccountUser", reqID);
    
            if (res != null && res.Status == "OK") {
                
                for (var i = 0; i < res.Results.length; i++) {
                    var user = res.Results[i];
                    var userName = user.Name || "Unknown";
                    var isActive = user.ActiveFlag; 
                    var objectUserId = user.UserID || ""; 
                    var userId = String(user.ID); 
                    var lastLogin = user.LastSuccessfulLogin || "Never Logged In"; 
                    var custkey = user.CustomerKey;
                    
                    var defaultBU = user.DefaultBusinessUnit ? String(user.DefaultBusinessUnit) : "";
    
                    var createdById = "";
                    if (user.Roles) {
                        var firstRole = user.Roles.length > 0 ? user.Roles[0] : null;
                        if (firstRole && firstRole.Client && firstRole.Client.CreatedBy != null) {
                            createdById = String(firstRole.Client.CreatedBy);
                        }
                    }
    
                    // Check validations
                    var isGuidName = guidRegex.test(userName);
                    var isObjectUserIdGuid = guidRegex.test(objectUserId); 
                    var isCreatedByZero = (createdById === "0");
                    var isInactive = (String(isActive).toLowerCase() === "false");
    
                    // --- EVALUATE CONDITIONS BASED ON DEBUG MODE ---
                    var passesAllChecks = false;
                    
                    if (debugMode) {
                        // If debugMode is true, write ALL returned users to the DE
                        passesAllChecks = true;
                    } else {
                        // Normal operation: Enforce all validation rules
                        passesAllChecks = (!isGuidName && !isInactive && isObjectUserIdGuid && !isCreatedByZero);
                    }
    
                    if (passesAllChecks) {
                        
                        var createdByName = createdById; 
                        
                        // --- 4. TRANSLATE CREATEDBY ID TO USERNAME ---
                        if (createdById !== "0" && createdById !== "") {
                            if (userCache[createdById]) {
                                createdByName = userCache[createdById];
                            } else {
                                var creatorRes = prox.retrieve("AccountUser", ["Name"], {
                                    Property: "ID",
                                    SimpleOperator: "equals",
                                    Value: createdById
                                });
                                
                                if (creatorRes && creatorRes.Status == "OK" && creatorRes.Results.length > 0) {
                                    createdByName = creatorRes.Results[0].Name;
                                    userCache[createdById] = createdByName; 
                                } else {
                                    userCache[createdById] = createdById; 
                                }
                            }
                        } else if (createdById === "0") {
                            createdByName = "System"; // Maps CreatedBy: 0 to "System" if debugMode allowed it through
                        }
    
                        // --- 5. INSERT RECORD ---
                        var insertProps = [
                            { Name: "UserID", Value: userId },
                            { Name: "UserName", Value: userName },
                            { Name: "LastLogin", Value: lastLogin },
                            { Name: "ApplicationID", Value: custkey},
                            { Name: "CreatedBy", Value: createdByName }, 
                            { Name: "DefaultBusinessUnit", Value: defaultBU }
                        ];
                        
                        var insertResult = prox.createItem("DataExtensionObject", {
                            CustomerKey: targetDEKey,
                            Properties: insertProps
                        });
                    }
                }
                
                if (res.HasMoreRows) {
                    moreData = true;
                    reqID = res.RequestID;
                }
            }
        }
        
    } catch (error) {
        // Left empty for Automation Studio
    }
    </script>
  6. Navigate to the Overview tab in Automation Studio and click New Automation.
  7. Drag a Schedule starting source onto the canvas.
  8. Drag a Script Activity onto the canvas and select the activity created in the previous section.
  9. Save the Automation.
  10. Click Run Once, select the Script Activity, and click Run.
  11. Navigate to Email Studio > Subscribers > Data Extensions to view the resulting records in the AppUser_LoginAudit Data Extension.

Notes and Troubleshooting

  • Script Exclusion Logic: To maintain a clean audit, the script actively excludes specific records. It bypasses users whose names consist entirely of system-generated GUIDs, UserID whose values consist entirely of system-generated GUIDs users designated with an inactive status (<ActiveFlag>false</ActiveFlag>), and system-generated roles identified by a creation value of zero (<CreatedBy>0</CreatedBy>).

  • Debug Mode: If troubleshooting is required to identify missing users, you can utilize the script's built-in debug mode. Locate the variable var debugMode = false; in the script and update the value to true. This action bypasses the CreatedBy == 0 restriction, allowing system-generated roles to write to the Data Extension for inspection. Revert this variable to false when troubleshooting is complete.

  • Data Overwrites: The script executes a ClearData command on the target Data Extension upon each run. Do not manually add records or append custom fields to the AppUser_LoginAudit Data Extension, as manual changes will be overwritten or cause script execution errors.

Løsning

Run the above SSJS activity to query the AccountUser object and log the login details of any matching users within the DE AppUser_LoginAudit.

This script natively includes WSProxy's getNextBatch pagination method. Even if you have thousands of users that match your search string, the script will safely page through them in batches of 2500 until all matching users have been processed and logged.

Knowledge-artikkelnummer

005317493

 
Laster
Salesforce Help | Article