diff --git a/.github/workflows/buildassetsimage.yml b/.github/workflows/buildassetsimage.yml
index 0a6b91766..307b81645 100644
--- a/.github/workflows/buildassetsimage.yml
+++ b/.github/workflows/buildassetsimage.yml
@@ -17,7 +17,9 @@ env:
ORG: turbot
CONFIG_SCHEMA_VERSION: "2020-11-18"
VERSION: ${{ github.event.inputs.tag }}
-
+ REACT_APP_STAGE: "production"
+ REACT_APP_HEAP_ID: "2696375185"
+
jobs:
build:
name: Build and Push Assets
@@ -60,6 +62,9 @@ jobs:
unset CI
yarn build
working-directory: ./ui/dashboard
+ env:
+ REACT_APP_STAGE: ${{ env.REACT_APP_STAGE }}
+ REACT_APP_HEAP_ID: ${{ env.REACT_APP_HEAP_ID }}
- name: Move Build Assets
run: |-
diff --git a/cloud/connection_string.go b/cloud/connection_string.go
index 5b8ae274a..e46c356e7 100644
--- a/cloud/connection_string.go
+++ b/cloud/connection_string.go
@@ -55,13 +55,15 @@ func GetCloudMetadata(workspaceDatabaseString, token string) (*steampipeconfig.C
connectionString := fmt.Sprintf("postgresql://%s:%s@%s-%s.%s:9193/%s", userHandle, password, identityHandle, workspaceHandle, workspaceHost, databaseName)
identity := workspace["identity"].(map[string]interface{})
- cloudMetadata := steampipeconfig.NewCloudMetadata()
+ cloudMetadata := steampipeconfig.NewCloudMetadata()
+ cloudMetadata.Actor.Id = userId
+ cloudMetadata.Actor.Handle = userHandle
cloudMetadata.Identity.Id = identity["id"].(string)
cloudMetadata.Identity.Type = identity["type"].(string)
cloudMetadata.Identity.Handle = identityHandle
- cloudMetadata.Actor.Id = userId
- cloudMetadata.Actor.Handle = userHandle
+ cloudMetadata.Workspace.Id = workspace["id"].(string)
+ cloudMetadata.Workspace.Handle = workspace["handle"].(string)
cloudMetadata.ConnectionString = connectionString
return cloudMetadata, nil
diff --git a/steampipeconfig/cloud_metadata.go b/steampipeconfig/cloud_metadata.go
index c938133a8..fb3ef8a69 100644
--- a/steampipeconfig/cloud_metadata.go
+++ b/steampipeconfig/cloud_metadata.go
@@ -1,15 +1,17 @@
package steampipeconfig
type CloudMetadata struct {
- Actor *ActorMetadata `json:"actor,omitempty"`
- Identity *IdentityMetadata `json:"identity,omitempty"`
- ConnectionString string `json:"-"`
+ Actor *ActorMetadata `json:"actor,omitempty"`
+ Identity *IdentityMetadata `json:"identity,omitempty"`
+ Workspace *WorkspaceMetadata `json:"workspace,omitempty"`
+ ConnectionString string `json:"-"`
}
func NewCloudMetadata() *CloudMetadata {
return &CloudMetadata{
- Actor: &ActorMetadata{},
- Identity: &IdentityMetadata{},
+ Actor: &ActorMetadata{},
+ Identity: &IdentityMetadata{},
+ Workspace: &WorkspaceMetadata{},
}
}
@@ -23,3 +25,8 @@ type IdentityMetadata struct {
Handle string `json:"handle,omitempty"`
Type string `json:"type,omitempty"`
}
+
+type WorkspaceMetadata struct {
+ Id string `json:"id,omitempty"`
+ Handle string `json:"handle,omitempty"`
+}
diff --git a/ui/dashboard/public/index.html b/ui/dashboard/public/index.html
index acefc5df9..42603ac2b 100644
--- a/ui/dashboard/public/index.html
+++ b/ui/dashboard/public/index.html
@@ -31,6 +31,45 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
Dashboards | Steampipe
+
+ <% if (process.env.REACT_APP_HEAP_ID) { %>
+
+ <% } %>
diff --git a/ui/dashboard/src/App.tsx b/ui/dashboard/src/App.tsx
index e0149031b..f47562a9d 100644
--- a/ui/dashboard/src/App.tsx
+++ b/ui/dashboard/src/App.tsx
@@ -2,16 +2,19 @@ import Dashboard from "./components/dashboards/layout/Dashboard";
import DashboardErrorModal from "./components/dashboards/DashboardErrorModal";
import DashboardHeader from "./components/DashboardHeader";
import DashboardList from "./components/DashboardList";
+import { AnalyticsProvider } from "./hooks/useAnalytics";
import { BreakpointProvider } from "./hooks/useBreakpoint";
import { DashboardProvider } from "./hooks/useDashboard";
import { Route, Routes } from "react-router-dom";
const DashboardApp = () => (
-
-
-
-
+
+
+
+
+
+
);
diff --git a/ui/dashboard/src/hooks/useAnalytics.tsx b/ui/dashboard/src/hooks/useAnalytics.tsx
new file mode 100644
index 000000000..9eb88adf0
--- /dev/null
+++ b/ui/dashboard/src/hooks/useAnalytics.tsx
@@ -0,0 +1,196 @@
+import usePrevious from "./usePrevious";
+import {
+ AvailableDashboard,
+ CloudDashboardIdentityMetadata,
+ CloudDashboardWorkspaceMetadata,
+ ModDashboardMetadata,
+ useDashboard,
+} from "./useDashboard";
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+} from "react";
+import { get } from "lodash";
+import { useTheme } from "./useTheme";
+
+interface AnalyticsProperties {
+ [key: string]: any;
+}
+
+interface IAnalyticsContext {
+ reset: () => void;
+ track: (string, AnalyticsProperties) => void;
+}
+
+interface SelectedDashboardStates {
+ selectedDashboard: AvailableDashboard | null;
+}
+
+const AnalyticsContext = createContext({
+ reset: () => {},
+ track: () => {},
+});
+
+const useAnalyticsProvider = () => {
+ const { metadata, metadataLoaded, selectedDashboard } = useDashboard();
+ const { localStorageTheme, theme } = useTheme();
+ const [enabled, setEnabled] = useState(true);
+ const [identity, setIdentity] =
+ useState(null);
+ const [workspace, setWorkspace] =
+ useState(null);
+ const [initialised, setInitialised] = useState(false);
+
+ const identify = useCallback(() => {
+ // @ts-ignore
+ window.heap && window.heap.identify(actor.id);
+ }, []);
+
+ const reset = useCallback(() => {
+ // @ts-ignore
+ window.heap && window.heap.resetIdentity();
+ }, []);
+
+ const track = useCallback(
+ (event, properties) => {
+ if (!initialised || !enabled) {
+ return;
+ }
+ const additionalProperties = {
+ theme: theme.name,
+ using_system_theme: !localStorageTheme,
+ };
+ if (identity) {
+ additionalProperties["identity.type"] = identity.type;
+ additionalProperties["identity.id"] = identity.id;
+ additionalProperties["identity.handle"] = identity.handle;
+ }
+ if (workspace) {
+ additionalProperties["workspace.id"] = workspace.id;
+ additionalProperties["workspace.handle"] = workspace.handle;
+ }
+ const finalProperties = {
+ ...additionalProperties,
+ ...properties,
+ };
+ // @ts-ignore
+ window.heap && window.heap.track(event, finalProperties);
+ },
+ [enabled, initialised, identity, workspace, localStorageTheme, theme]
+ );
+
+ useEffect(() => {
+ if (!metadataLoaded) {
+ return;
+ }
+
+ setEnabled(
+ metadata.telemetry === "info" && !!process.env.REACT_APP_HEAP_ID
+ );
+
+ if (metadata.telemetry !== "info") {
+ } else {
+ // @ts-ignore
+ if (window.heap) {
+ // @ts-ignore
+ window.heap.load(process.env.REACT_APP_HEAP_ID);
+ }
+ }
+
+ setInitialised(true);
+ }, [metadataLoaded, metadata]);
+
+ useEffect(() => {
+ if (!metadataLoaded || !initialised) {
+ return;
+ }
+
+ const cloudMetadata = metadata.cloud;
+
+ const identity = cloudMetadata?.identity;
+ const workspace = cloudMetadata?.workspace;
+
+ setIdentity(identity ? identity : null);
+ setWorkspace(workspace ? workspace : null);
+
+ const actor = cloudMetadata?.actor;
+
+ if (actor && enabled) {
+ identify();
+ } else if (enabled) {
+ reset();
+ }
+ }, [metadataLoaded, metadata, enabled, initialised]);
+
+ // @ts-ignore
+ const previousSelectedDashboardStates: SelectedDashboardStates = usePrevious({
+ selectedDashboard,
+ });
+
+ useEffect(() => {
+ if (!enabled) {
+ return;
+ }
+
+ if (
+ ((!previousSelectedDashboardStates ||
+ !previousSelectedDashboardStates.selectedDashboard) &&
+ selectedDashboard) ||
+ (previousSelectedDashboardStates &&
+ previousSelectedDashboardStates.selectedDashboard &&
+ selectedDashboard &&
+ previousSelectedDashboardStates.selectedDashboard.full_name !==
+ selectedDashboard?.full_name)
+ ) {
+ let mod: ModDashboardMetadata;
+ if (selectedDashboard.mod_full_name === metadata.mod.full_name) {
+ mod = get(metadata, "mod", {} as ModDashboardMetadata);
+ } else {
+ mod = get(
+ metadata,
+ `installed_mods["${selectedDashboard.mod_full_name}"]`,
+ {} as ModDashboardMetadata
+ );
+ }
+ track("cli.ui.dashboard.select", {
+ "mod.title": mod
+ ? mod.title
+ ? mod.title
+ : mod.short_name
+ : selectedDashboard.mod_full_name,
+ "mod.name": mod ? mod.short_name : selectedDashboard.mod_full_name,
+ dashboard: selectedDashboard.short_name,
+ });
+ }
+ }, [enabled, metadata, previousSelectedDashboardStates, selectedDashboard]);
+
+ return {
+ reset,
+ track,
+ };
+};
+
+const AnalyticsProvider = ({ children }) => {
+ const analytics = useAnalyticsProvider();
+
+ return (
+
+ {children}
+
+ );
+};
+
+const useAnalytics = () => {
+ const context = useContext(AnalyticsContext);
+ if (context === undefined) {
+ throw new Error("useAnalytics must be used within an AnalyticsContext");
+ }
+ return context;
+};
+
+export default useAnalytics;
+
+export { AnalyticsProvider };
diff --git a/ui/dashboard/src/hooks/useDashboard.tsx b/ui/dashboard/src/hooks/useDashboard.tsx
index 3d76b1f2a..53fc0decd 100644
--- a/ui/dashboard/src/hooks/useDashboard.tsx
+++ b/ui/dashboard/src/hooks/useDashboard.tsx
@@ -58,9 +58,33 @@ interface InstalledModsDashboardMetadata {
[key: string]: ModDashboardMetadata;
}
+export interface CloudDashboardActorMetadata {
+ id: string;
+ handle: string;
+}
+
+export interface CloudDashboardIdentityMetadata {
+ id: string;
+ handle: string;
+ type: "org" | "user";
+}
+
+export interface CloudDashboardWorkspaceMetadata {
+ id: string;
+ handle: string;
+}
+
+interface CloudDashboardMetadata {
+ actor: CloudDashboardActorMetadata;
+ identity: CloudDashboardIdentityMetadata;
+ workspace: CloudDashboardWorkspaceMetadata;
+}
+
interface DashboardMetadata {
mod: ModDashboardMetadata;
- installed_mods: InstalledModsDashboardMetadata;
+ installed_mods?: InstalledModsDashboardMetadata;
+ cloud?: CloudDashboardMetadata;
+ telemetry: "info" | "none";
}
interface AvailableDashboardTags {
diff --git a/ui/dashboard/src/utils/storybook.tsx b/ui/dashboard/src/utils/storybook.tsx
index f80f8af5b..45b8c058c 100644
--- a/ui/dashboard/src/utils/storybook.tsx
+++ b/ui/dashboard/src/utils/storybook.tsx
@@ -27,6 +27,7 @@ export const PanelStoryDecorator = ({
short_name: "storybook",
},
installed_mods: {},
+ telemetry: "none",
},
metadataLoaded: true,
availableDashboardsLoaded: true,
diff --git a/ui/dashboard/yarn.lock b/ui/dashboard/yarn.lock
index 51ecc78b1..2412253d3 100644
--- a/ui/dashboard/yarn.lock
+++ b/ui/dashboard/yarn.lock
@@ -5297,7 +5297,7 @@ ajv-keywords@^5.0.0:
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5:
version "6.12.6"
- resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies:
fast-deep-equal "^3.1.1"
@@ -6847,7 +6847,7 @@ colors@^1.1.2:
combined-stream@^1.0.8:
version "1.0.8"
- resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
@@ -7086,7 +7086,7 @@ core-js@^3.8.2:
core-util-is@~1.0.0:
version "1.0.2"
- resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
cosmiconfig-typescript-loader@^1.0.0:
@@ -8766,7 +8766,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2:
extend@^3.0.0:
version "3.0.2"
- resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
extglob@^2.0.4:
@@ -10583,7 +10583,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3:
is-typedarray@^1.0.0:
version "1.0.0"
- resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-utf8@^0.2.0:
@@ -11325,7 +11325,7 @@ json-schema-traverse@^1.0.0:
json-schema@^0.4.0:
version "0.4.0"
- resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
json-stable-stringify-without-jsonify@^1.0.1:
@@ -12427,7 +12427,7 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24:
mime-types@^2.1.30, mime-types@^2.1.31:
version "2.1.34"
- resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24"
integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==
dependencies:
mime-db "1.51.0"
@@ -14379,7 +14379,7 @@ prr@~1.0.1:
psl@^1.1.33:
version "1.8.0"
- resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
public-encrypt@^4.0.0:
@@ -15486,7 +15486,7 @@ safe-regex@^1.1.0:
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0:
version "2.1.2"
- resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sane@^4.0.3:
@@ -17486,7 +17486,7 @@ uuid@8.3.2:
uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0"
- resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uvu@^0.5.0: