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: