-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Initial commit of custom objects, layouts, flexipages, console app, permission sets, and Apex schedulable batch job
- Loading branch information
Showing
76 changed files
with
3,594 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
# List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status | ||
# More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm | ||
# | ||
|
||
package.xml | ||
|
||
# LWC configuration files | ||
**/jsconfig.json | ||
**/.eslintrc.json | ||
|
||
# LWC Jest | ||
**/__tests__/** |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# Folders to exclude | ||
.localdevserver/ | ||
.settings/ | ||
.sfdx/ | ||
.vscode/ | ||
wip/ | ||
|
||
# NPM | ||
node_modules/ | ||
yarn.lock | ||
|
||
# Files to exclude | ||
*.log | ||
**/lwc/jsconfig.json |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# List files or directories below to ignore them when running prettier | ||
# More information: https://prettier.io/docs/en/ignore.html | ||
# | ||
|
||
.sfdx | ||
.vscode |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"trailingComma": "none", | ||
"tabWidth": 2, | ||
"useTabs": false, | ||
"printWidth": 120, | ||
"overrides": [ | ||
{ | ||
"files": "**/lwc/**/*.html", | ||
"options": { "parser": "lwc" } | ||
}, | ||
{ | ||
"files": "*.{cmp,page,component}", | ||
"options": { "parser": "html" } | ||
} | ||
] | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Package Analytics | ||
|
||
Provides reportable data about your Salesforce packages in your dev hub. | ||
|
||
[![Install Unlocked Package in a Dev Hub](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t4x000000FEjUAAW) | ||
|
||
## Why? | ||
|
||
When working with packages in Salesforce, the platform automatically tracks & stores data about your packages, package versions, and customer orgs that are using your package(s) (referred to as 'subscriber orgs'). However, the objects that store this data can only be accessed via SOQL or Apex - they are not accessible via the Salesforce UI, which also means that the data is not reportable. | ||
|
||
This project aims to help ISVs better monitor their packages by extracting data into custom objects so you can easily view it in your dev hub, using standard Salesforce UI functionality. The data is also reportable, using standard Salesforce report & dashboard functionality. | ||
|
||
## Getting Started | ||
|
||
1. Install the package into your dev hub | ||
2. Schedule the Apex job using `new PackageDataExtractJob().scheduleHourly();`. This will also immediately run the job. | ||
3. Optional: assign permission sets to any users in your dev hub that should be able to see/report on the data. | ||
- System Admins should already have access to the objects/data, so they should not need to have any permission sets assigned | ||
- 'Package Analytics Admin' permission set - for any users that should be able to modify or delete access to the data in the included custom objects | ||
- 'Package Analytics Viewer' permission set - for any users that should have read-only access to the data in the included custom objects | ||
4. Open the app 'Package Analytics' in App Switcher | ||
5. Enjoy the reportable data about your packages. You can build your own reports & dashboards, using these custom objects | ||
- `Package__c` | ||
- `PackageVersion__c` | ||
- `PackageSubscriberOrg__c` | ||
|
||
## Screenshots | ||
|
||
View your list of 1GP and 2GP packages | ||
|
||
![Packages list view](/images/packages-tab.png) | ||
|
||
For each package, you can see the list of package versions & additional details about the package | ||
|
||
![Package detail page](/images/package-detail-page.png) | ||
|
||
Similarly, for each package version, you can see the list of orgs that have installed the package version, as well as additional details about the package version. | ||
|
||
![Package Version detail page](/images/package-version-detail-page.png) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"orgName": "Package Analytics - Base Scratch Org", | ||
"edition": "Enterprise", | ||
"hasSampleData": true, | ||
"country": "US", | ||
"language": "en_US", | ||
"features": [], | ||
"settings": { | ||
"userManagementSettings": { | ||
"enableEnhancedPermsetMgmt": true, | ||
"enableEnhancedProfileMgmt": true, | ||
"enableNewProfileUI": true | ||
} | ||
} | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
38 changes: 38 additions & 0 deletions
38
package-analytics/main/default/applications/PackageAnalyticsConsole.app-meta.xml
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?xml version="1.0" encoding="UTF-8" ?> | ||
<CustomApplication xmlns="http://soap.sforce.com/2006/04/metadata"> | ||
<brand> | ||
<headerColor>#26AE19</headerColor> | ||
<shouldOverrideOrgTheme>false</shouldOverrideOrgTheme> | ||
</brand> | ||
<formFactors>Small</formFactors> | ||
<formFactors>Large</formFactors> | ||
<isNavAutoTempTabsDisabled>false</isNavAutoTempTabsDisabled> | ||
<isNavPersonalizationDisabled>false</isNavPersonalizationDisabled> | ||
<label>Package Analytics</label> | ||
<navType>Console</navType> | ||
<tabs>Package__c</tabs> | ||
<tabs>PackageVersion__c</tabs> | ||
<tabs>PackageSubscriberOrg__c</tabs> | ||
<tabs>standard-report</tabs> | ||
<tabs>standard-Dashboard</tabs> | ||
<uiType>Lightning</uiType> | ||
<utilityBar>PackageAnalyticsConsoleUtilityBar</utilityBar> | ||
<workspaceConfig> | ||
<mappings> | ||
<fieldName>PackageVersion__c</fieldName> | ||
<tab>PackageSubscriberOrg__c</tab> | ||
</mappings> | ||
<mappings> | ||
<tab>PackageVersion__c</tab> | ||
</mappings> | ||
<mappings> | ||
<tab>Package__c</tab> | ||
</mappings> | ||
<mappings> | ||
<tab>standard-Dashboard</tab> | ||
</mappings> | ||
<mappings> | ||
<tab>standard-report</tab> | ||
</mappings> | ||
</workspaceConfig> | ||
</CustomApplication> |
249 changes: 249 additions & 0 deletions
249
package-analytics/main/default/classes/PackageDataExtractJob.cls
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
//-------------------------------------------------------------------------------------------------// | ||
// This file is part of the Package Analytics project, released under the MIT License. // | ||
// See LICENSE file or go to https://github.com/jongpie/PackageAnalytics for full license details. // | ||
//-------------------------------------------------------------------------------------------------// | ||
|
||
public without sharing class PackageDataExtractJob implements Database.Batchable<SObject>, Database.Stateful, Schedulable { | ||
private static final Integer BATCH_SIZE = 2000; | ||
@TestVisible | ||
private static final String DEFAULT_HOURLY_JOB_NAME = 'Hourly Package Data Extract Job'; | ||
private static final Database.DmlOptions DML_OPTIONS { | ||
get { | ||
if (DML_OPTIONS == null) { | ||
DML_OPTIONS = new Database.DmlOptions(); | ||
DML_OPTIONS.AllowFieldTruncation = true; | ||
} | ||
return DML_OPTIONS; | ||
} | ||
set; | ||
} | ||
@TestVisible | ||
private static final String HOURLY_CRON_SCHEDULE = '0 0 * * * ?'; | ||
@TestVisible | ||
private static final Boolean IS_PACKAGING_ORG = Type.forName('MetadataPackage') != null; | ||
@TestVisible | ||
private static final String METADATA_PACKAGE_NAME = 'MetadataPackage'; | ||
@TestVisible | ||
private static final String METADATA_PACKAGE_QUERY = 'SELECT Id, Name, NamespacePrefix, PackageCategory, SystemModStamp FROM MetadataPackage ORDER BY SystemModStamp DESC'; | ||
@TestVisible | ||
private static final String METADATA_PACKAGE_VERSION_NAME = 'MetadataPackageVersion'; | ||
@TestVisible | ||
private static final String METADATA_PACKAGE_VERSION_QUERY = 'SELECT BuildNumber, Id, IsDeprecated, MajorVersion, MetadataPackageid, MinorVersion, Name, PatchVersion, ReleaseState, SystemModStamp FROM MetadataPackageVersion ORDER BY SystemModStamp DESC'; | ||
@TestVisible | ||
private static final String PACKAGE_SUBSCRIBER_NAME = 'PackageSubscriber'; | ||
@TestVisible | ||
private static final String PACKAGE_SUBSCRIBER_QUERY = 'SELECT Id, InstanceName, MetadataPackageId, MetadataPackageVersionId, OrgKey, OrgName, OrgStatus, OrgType, ParentOrg, SystemModStamp FROM PackageSubscriber ORDER BY SystemModStamp DESC'; | ||
|
||
@TestVisible | ||
private String currentQuery; | ||
@TestVisible | ||
private String currentSObjectName; | ||
@TestVisible | ||
private Integer queryLimit; | ||
@TestVisible | ||
private List<String> sobjectNames = new List<String>{ | ||
METADATA_PACKAGE_NAME, | ||
METADATA_PACKAGE_VERSION_NAME, | ||
PACKAGE_SUBSCRIBER_NAME | ||
}; | ||
@TestVisible | ||
private Boolean willRunAnotherTime = false; | ||
|
||
public Id scheduleHourly() { | ||
return this.scheduleHourly(DEFAULT_HOURLY_JOB_NAME, true); | ||
} | ||
|
||
public Id scheduleHourly(String jobName, Boolean runImmediately) { | ||
if (runImmediately == true) { | ||
this.executeBatch(); | ||
} | ||
return System.schedule(jobName, HOURLY_CRON_SCHEDULE, this); | ||
} | ||
|
||
public void execute(System.SchedulableContext context) { | ||
this.executeBatch(); | ||
} | ||
|
||
public Database.QueryLocator start(Database.BatchableContext context) { | ||
this.currentSObjectName = this.sobjectNames.remove(0); | ||
return getQueryLocator(); | ||
} | ||
|
||
public void execute(Database.BatchableContext context, List<Object> scope) { | ||
// Switch statements are amazing, but in Apex, you would have to use an inline hardcoded string, which is pretty lame | ||
if (this.currentSObjectName == METADATA_PACKAGE_NAME) { | ||
List<MetadataPackageInfo> packages = (List<MetadataPackageInfo>) this.deserializeTo( | ||
scope, | ||
List<MetadataPackageInfo>.class | ||
); | ||
this.processPackages(packages); | ||
} else if (this.currentSObjectName == METADATA_PACKAGE_VERSION_NAME) { | ||
List<MetadataPackageVersionInfo> packageVersions = (List<MetadataPackageVersionInfo>) this.deserializeTo( | ||
scope, | ||
List<MetadataPackageVersionInfo>.class | ||
); | ||
this.processPackageVersions(packageVersions); | ||
} else if (this.currentSObjectName == PACKAGE_SUBSCRIBER_NAME) { | ||
List<PackageSubscriberInfo> packageSubscribers = (List<PackageSubscriberInfo>) this.deserializeTo( | ||
scope, | ||
List<PackageSubscriberInfo>.class | ||
); | ||
this.processPackageSubscribers(packageSubscribers); | ||
} | ||
} | ||
|
||
public void finish(Database.BatchableContext context) { | ||
if (this.sobjectNames.isEmpty() == false) { | ||
this.willRunAnotherTime = true; | ||
if (Test.isRunningTest() == false) { | ||
this.executeBatch(); | ||
} | ||
} | ||
} | ||
|
||
private void executeBatch() { | ||
Database.executeBatch(this, BATCH_SIZE); | ||
} | ||
|
||
private Database.QueryLocator getQueryLocator() { | ||
String query; | ||
// Switch statements are amazing, but in Apex, you would have to use an inline hardcoded string, which is pretty lame | ||
if (this.currentSObjectName == METADATA_PACKAGE_NAME) { | ||
query = METADATA_PACKAGE_QUERY; | ||
} else if (this.currentSObjectName == METADATA_PACKAGE_VERSION_NAME) { | ||
query = METADATA_PACKAGE_VERSION_QUERY; | ||
} else if (this.currentSObjectName == PACKAGE_SUBSCRIBER_NAME) { | ||
query = PACKAGE_SUBSCRIBER_QUERY; | ||
} else { | ||
Exception ex = new IllegalArgumentException(); | ||
ex.setMessage('Unsupported SObjectType: ' + this.currentSObjectName); | ||
throw ex; | ||
} | ||
|
||
if (this.queryLimit != null) { | ||
query += ' LIMIT ' + this.queryLimit; | ||
} | ||
|
||
// A horrible hack - when the 3 SObjects (above) don't exist in the org, the start() method fails because | ||
// the QueryLocator can't find the objects, and scratch orgs (used to create the 2GP) & sandboxes will never have the 3 SObjects. | ||
// Overriding the query string keeps the platform happy, and unit tests check the value of this.query as a workaround | ||
this.currentQuery = query; | ||
if (Test.isRunningTest() == true && IS_PACKAGING_ORG == false) { | ||
query = 'SELECT Id FROM User'; | ||
} | ||
return Database.getQueryLocator(query); | ||
} | ||
|
||
private List<Object> deserializeTo(List<Object> scope, Type type) { | ||
return (List<Object>) JSON.deserialize(JSON.serialize(scope), type); | ||
} | ||
|
||
private void processPackages(List<MetadataPackageInfo> metadataPackages) { | ||
List<Package__c> storedPackages = new List<Package__c>(); | ||
for (MetadataPackageInfo metadataPackage : metadataPackages) { | ||
Package__c storedPackage = new Package__c( | ||
LastUpdated__c = metadataPackage.SystemModStamp, | ||
PackageCategory__c = metadataPackage.PackageCategory, | ||
PackageId__c = metadataPackage.Id, | ||
Name = metadataPackage.Name, | ||
NamespacePrefix__c = metadataPackage.NamespacePrefix | ||
); | ||
storedPackage.setOptions(DML_OPTIONS); | ||
storedPackages.add(storedPackage); | ||
} | ||
upsert storedPackages PackageId__c; | ||
} | ||
|
||
private void processPackageVersions(List<MetadataPackageVersionInfo> metadataPackageVersions) { | ||
List<PackageVersion__c> storedPackageVersions = new List<PackageVersion__c>(); | ||
for (MetadataPackageVersionInfo metadataPackageVersion : metadataPackageVersions) { | ||
PackageVersion__c storedPackageVersion = new PackageVersion__c( | ||
BuildNumber__c = metadataPackageVersion.BuildNumber, | ||
IsDeprecated__c = metadataPackageVersion.IsDeprecated, | ||
LastUpdated__c = metadataPackageVersion.SystemModStamp, | ||
MajorVersion__c = metadataPackageVersion.MajorVersion, | ||
MinorVersion__c = metadataPackageVersion.MinorVersion, | ||
Name = metadataPackageVersion.Name, | ||
Package__r = new Package__c(PackageId__c = metadataPackageVersion.MetadataPackageId), | ||
PatchVersion__c = metadataPackageVersion.PatchVersion, | ||
ReleaseState__c = metadataPackageVersion.ReleaseState, | ||
SubscriberPackageVersionId__c = metadataPackageVersion.Id | ||
); | ||
storedPackageVersion.setOptions(DML_OPTIONS); | ||
storedPackageVersions.add(storedPackageVersion); | ||
} | ||
upsert storedPackageVersions SubscriberPackageVersionId__c; | ||
} | ||
|
||
private void processPackageSubscribers(List<PackageSubscriberInfo> packageSubscribers) { | ||
Map<String, PackageSubscriberOrg__c> parentSubscriberOrgsByOrgId = new Map<String, PackageSubscriberOrg__c>(); | ||
Map<String, PackageSubscriberOrg__c> childSubscriberOrgsByOrgId = new Map<String, PackageSubscriberOrg__c>(); | ||
|
||
for (PackageSubscriberInfo packageSubscriber : packageSubscribers) { | ||
PackageSubscriberOrg__c storedOrg = new PackageSubscriberOrg__c( | ||
InstanceName__c = packageSubscriber.InstanceName, | ||
LastUpdated__c = packageSubscriber.SystemModStamp, | ||
Name = packageSubscriber.OrgName, | ||
OrgId__c = packageSubscriber.OrgKey, | ||
OrgStatus__c = packageSubscriber.OrgStatus, | ||
OrgType__c = packageSubscriber.OrgType, | ||
PackageSubscriberId__c = packageSubscriber.Id, | ||
PackageVersion__r = new PackageVersion__c( | ||
SubscriberPackageVersionId__c = packageSubscriber.MetadataPackageVersionId | ||
) | ||
); | ||
if (packageSubscriber.ParentOrg == null) { | ||
parentSubscriberOrgsByOrgId.put(storedOrg.OrgId__c, storedOrg); | ||
} else { | ||
storedOrg.ParentOrgId__c = packageSubscriber.ParentOrg; | ||
childSubscriberOrgsByOrgId.put(storedOrg.OrgId__c, storedOrg); | ||
} | ||
} | ||
|
||
// Parent orgs | ||
upsert parentSubscriberOrgsByOrgId.values() OrgId__c; | ||
|
||
// Child orgs | ||
for (PackageSubscriberOrg__c childOrg : childSubscriberOrgsByOrgId.values()) { | ||
if (parentSubscriberOrgsByOrgId.containsKey(childOrg.ParentOrgId__c) == true) { | ||
childOrg.ParentOrg__c = parentSubscriberOrgsByOrgId.get(childOrg.ParentOrgId__c).Id; | ||
} | ||
} | ||
upsert childSubscriberOrgsByOrgId.values() OrgId__c; | ||
} | ||
|
||
// Inner classes used to substitute the SObjects `MetadataPackage`, `MetadataPackageVersion`, and `PackageSubscriber` | ||
// that don't/won't/can't exist in scratch orgs (used for creating package versions) or sandboxes | ||
public class MetadataPackageInfo { | ||
public Id Id; | ||
public String Name; | ||
public String NamespacePrefix; | ||
public String PackageCategory; | ||
public Datetime SystemModStamp; | ||
} | ||
|
||
public class MetadataPackageVersionInfo { | ||
public Decimal BuildNumber; | ||
public Id Id; | ||
public Boolean IsDeprecated; | ||
public Decimal MajorVersion; | ||
public String MetadataPackageId; | ||
public Decimal MinorVersion; | ||
public String Name; | ||
public Decimal PatchVersion; | ||
public String ReleaseState; | ||
public Datetime SystemModStamp; | ||
} | ||
|
||
public class PackageSubscriberInfo { | ||
public Id Id; | ||
public String InstanceName; | ||
public Id MetadataPackageVersionId; | ||
public Id OrgKey; | ||
public String OrgName; | ||
public String OrgStatus; | ||
public String OrgType; | ||
public Id ParentOrg; | ||
public Datetime SystemModStamp; | ||
} | ||
} |
Oops, something went wrong.