Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions cmd/workspace/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/devsy-org/devsy/pkg/client/clientimplementation"
"github.com/devsy-org/devsy/pkg/config"
"github.com/devsy-org/devsy/pkg/log"
"github.com/devsy-org/devsy/pkg/telemetry"
"github.com/devsy-org/devsy/pkg/workspace"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -68,10 +69,19 @@ func (cmd *DeleteCmd) Run(cobraCmd *cobra.Command, args []string) error {

ctx := cobraCmd.Context()
if len(args) <= 1 {
return cmd.deleteSingle(ctx, devsyConfig, args)
err = cmd.deleteSingle(ctx, devsyConfig, args)
} else {
err = cmd.deleteMultiple(ctx, devsyConfig, args)
}

return cmd.deleteMultiple(ctx, devsyConfig, args)
count, countErr := workspace.CountLocalWorkspaces(devsyConfig.DefaultContext)
if countErr != nil {
log.Debugf("skipping workspace count gauge: %v", countErr)
} else {
telemetry.FromContext(ctx).RecordWorkspaceGauge(count)
}

return err
}

func (cmd *DeleteCmd) loadConfig() (*config.Config, error) {
Expand Down
22 changes: 20 additions & 2 deletions cmd/workspace/up/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
provider2 "github.com/devsy-org/devsy/pkg/provider"
"github.com/devsy-org/devsy/pkg/telemetry"
"github.com/devsy-org/devsy/pkg/util"
"github.com/devsy-org/devsy/pkg/workspace"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -93,7 +94,20 @@ func RunFromOptions(ctx context.Context, g *flags.GlobalFlags, opts Options) err
return fmt.Errorf("extra devcontainer file is only supported with local provider")
}
telemetry.FromContext(ctx).SetClient(client)
return cmd.Run(ctx, devsyConfig, client, args)
if err := cmd.Run(ctx, devsyConfig, client, args); err != nil {
return err
}
recordWorkspaceGauge(ctx, devsyConfig)
return nil
}

func recordWorkspaceGauge(ctx context.Context, devsyConfig *config.Config) {
count, err := workspace.CountLocalWorkspaces(devsyConfig.DefaultContext)
if err != nil {
log.Debugf("skipping workspace count gauge: %v", err)
return
}
telemetry.FromContext(ctx).RecordWorkspaceGauge(count)
}

func buildUpCmd(g *flags.GlobalFlags, opts Options) *UpCmd {
Expand Down Expand Up @@ -298,7 +312,11 @@ func (cmd *UpCmd) execute(cobraCmd *cobra.Command, args []string) error {
}

telemetry.FromContext(cobraCmd.Context()).SetClient(client)
return cmd.Run(ctx, devsyConfig, client, args)
if err := cmd.Run(ctx, devsyConfig, client, args); err != nil {
return err
}
recordWorkspaceGauge(ctx, devsyConfig)
return nil
}

// workspaceContext holds the result of workspace preparation.
Expand Down
37 changes: 30 additions & 7 deletions desktop/src/main/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,23 @@ describe("CliRunner", () => {
},
)

const result = await cli.run<{ id: string }[]>(["workspace", "list", "--skip-pro"])
const result = await cli.run<{ id: string }[]>([
"workspace",
"list",
"--skip-pro",
])
expect(result).toEqual([{ id: "ws-1" }])
expect(mockExecFile).toHaveBeenCalledWith(
"/usr/local/bin/devsy",
["workspace", "list", "--skip-pro", "--result-format", "json", "--log-output", "json"],
[
"workspace",
"list",
"--skip-pro",
"--result-format",
"json",
"--log-output",
"json",
],
expect.objectContaining({ env: expect.any(Object) }),
expect.any(Function),
)
Expand All @@ -57,7 +69,9 @@ describe("CliRunner", () => {
},
)

await expect(cli.run(["workspace", "list"])).rejects.toThrow("workspace not found")
await expect(cli.run(["workspace", "list"])).rejects.toThrow(
"workspace not found",
)
})

it("extracts cliError from a zap JSON stderr line and attaches it to the thrown Error", async () => {
Expand All @@ -68,9 +82,11 @@ describe("CliRunner", () => {
code: "AWS_PROFILE_MISSING",
message: "AWS credentials are not configured.",
hint: "Set AWS_PROFILE or create ~/.aws/credentials.",
docUrl: "https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html",
docUrl:
"https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html",
provider: "aws",
cause: "init: exit status 1: failed to get shared config profile, default",
cause:
"init: exit status 1: failed to get shared config profile, default",
}
const stderrLine = JSON.stringify({
level: "error",
Expand All @@ -91,7 +107,7 @@ describe("CliRunner", () => {
)

const rejection = await cli
.run(["provider", "set", "aws"])
.run<never>(["provider", "set", "aws"])
.catch((e) => e as Error & { cliError?: typeof cliErrorPayload })
expect(rejection).toBeInstanceOf(Error)
expect(rejection.cliError).toEqual(cliErrorPayload)
Expand Down Expand Up @@ -130,7 +146,14 @@ describe("CliRunner", () => {
await jsCli.run(["list"])
expect(mockExecFile).toHaveBeenCalledWith(
"node",
["/tmp/mock.cjs", "list", "--result-format", "json", "--log-output", "json"],
[
"/tmp/mock.cjs",
"list",
"--result-format",
"json",
"--log-output",
"json",
],
expect.objectContaining({ env: expect.any(Object) }),
expect.any(Function),
)
Expand Down
77 changes: 58 additions & 19 deletions desktop/src/main/__tests__/updater.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
import { beforeEach, describe, expect, it, vi } from "vitest"

const electronUpdaterMock = {
autoUpdater: {
Expand Down Expand Up @@ -76,37 +76,56 @@ describe("updater", () => {
"update-status",
expect.objectContaining({
state: "downloading",
progress: { percent: 42, bytesPerSecond: 1000, transferred: 100, total: 200 },
progress: {
percent: 42,
bytesPerSecond: 1000,
transferred: 100,
total: 200,
},
}),
)
})

it("respects autoDownload setting on update-available", async () => {
const { initAutoUpdater, setAutoDownloadEnabled } = await import("../updater.js")
const { initAutoUpdater, setAutoDownloadEnabled } = await import(
"../updater.js"
)
const send = vi.fn()
const win = { isDestroyed: () => false, webContents: { send } } as never
await initAutoUpdater(() => win)
setAutoDownloadEnabled(false)
electronUpdaterMock.autoUpdater.emit("update-available", { version: "9.9.9" })
expect(electronUpdaterMock.autoUpdater.downloadUpdate).not.toHaveBeenCalled()
electronUpdaterMock.autoUpdater.emit("update-available", {
version: "9.9.9",
})
expect(
electronUpdaterMock.autoUpdater.downloadUpdate,
).not.toHaveBeenCalled()
})

it("sets app.isQuitting before quitAndInstall so the window can close", async () => {
const electron = await import("electron")
;(electron.app as { isQuitting: boolean }).isQuitting = false
;(
electron.app as typeof electron.app & { isQuitting?: boolean }
).isQuitting = false
let quittingWhenInstalled: boolean | undefined
electronUpdaterMock.autoUpdater.quitAndInstall.mockImplementation(() => {
quittingWhenInstalled = (electron.app as { isQuitting: boolean }).isQuitting
quittingWhenInstalled = (
electron.app as typeof electron.app & { isQuitting?: boolean }
).isQuitting
})
const { installUpdate } = await import("../updater.js")
await installUpdate()
expect(quittingWhenInstalled).toBe(true)
expect(electronUpdaterMock.autoUpdater.quitAndInstall).toHaveBeenCalledTimes(1)
expect(
electronUpdaterMock.autoUpdater.quitAndInstall,
).toHaveBeenCalledTimes(1)
})

it("swallows a channel-missing rejection from check_for_updates", async () => {
electronUpdaterMock.autoUpdater.checkForUpdates.mockRejectedValueOnce(
new Error('Cannot find channel "latest-mac.yml" update info: HttpError: 404'),
new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404',
),
)
const { checkForUpdates } = await import("../updater.js")
await expect(checkForUpdates()).resolves.toBeUndefined()
Expand All @@ -126,8 +145,12 @@ describe("updater", () => {
const send = vi.fn()
const win = { isDestroyed: () => false, webContents: { send } } as never
await initAutoUpdater(() => win)
electronUpdaterMock.autoUpdater.emit("update-downloaded", { version: "9.9.9" })
expect((electron.dialog.showMessageBox as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled()
electronUpdaterMock.autoUpdater.emit("update-downloaded", {
version: "9.9.9",
})
expect(
electron.dialog.showMessageBox as ReturnType<typeof vi.fn>,
).not.toHaveBeenCalled()
})

it("checks at boot and again on the recheck interval", async () => {
Expand All @@ -138,16 +161,24 @@ describe("updater", () => {
const win = { isDestroyed: () => false, webContents: { send } } as never
await initAutoUpdater(() => win)

expect(electronUpdaterMock.autoUpdater.checkForUpdates).not.toHaveBeenCalled()
expect(
electronUpdaterMock.autoUpdater.checkForUpdates,
).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(10_000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1)
expect(
electronUpdaterMock.autoUpdater.checkForUpdates,
).toHaveBeenCalledTimes(1)

await vi.advanceTimersByTimeAsync(6 * 60 * 60 * 1000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(2)
expect(
electronUpdaterMock.autoUpdater.checkForUpdates,
).toHaveBeenCalledTimes(2)

stopAutoUpdater()
await vi.advanceTimersByTimeAsync(6 * 60 * 60 * 1000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(2)
expect(
electronUpdaterMock.autoUpdater.checkForUpdates,
).toHaveBeenCalledTimes(2)
} finally {
vi.useRealTimers()
}
Expand All @@ -164,7 +195,9 @@ describe("updater", () => {
// Quit before the 10s boot delay elapses.
stopAutoUpdater()
await vi.advanceTimersByTimeAsync(10_000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).not.toHaveBeenCalled()
expect(
electronUpdaterMock.autoUpdater.checkForUpdates,
).not.toHaveBeenCalled()
} finally {
vi.useRealTimers()
}
Expand All @@ -179,11 +212,17 @@ describe("updater", () => {
await initAutoUpdater(() => win)

await vi.advanceTimersByTimeAsync(10_000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1)
expect(
electronUpdaterMock.autoUpdater.checkForUpdates,
).toHaveBeenCalledTimes(1)

electronUpdaterMock.autoUpdater.emit("update-downloaded", { version: "9.9.9" })
electronUpdaterMock.autoUpdater.emit("update-downloaded", {
version: "9.9.9",
})
await vi.advanceTimersByTimeAsync(6 * 60 * 60 * 1000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1)
expect(
electronUpdaterMock.autoUpdater.checkForUpdates,
).toHaveBeenCalledTimes(1)

stopAutoUpdater()
} finally {
Expand Down
17 changes: 15 additions & 2 deletions desktop/src/main/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,25 @@ export function getAnalyticsDistinctId(): string {
return distinctId || getDistinctId()
}

export function hashWorkspaceRef(workspaceId: string): string {
if (!client) return ""
return createHmac("sha256", getAnalyticsDistinctId())
.update(workspaceId)
.digest("hex")
.slice(0, 16)
}

// machineIdSync spawns a subprocess; cache so it runs at most once per process.
let cachedDistinctId = ""

function getDistinctId(): string {
if (cachedDistinctId) return cachedDistinctId
const id = machineIdSync()
const home = homedir()
const mac = createHmac("sha256", id)
mac.update(home)
return mac.digest("hex")
cachedDistinctId = mac.digest("hex")
return cachedDistinctId
}

function isTelemetryDisabled(): boolean {
Expand All @@ -33,7 +46,7 @@ function isTelemetryDisabled(): boolean {
export function initAnalytics(): void {
if (isTelemetryDisabled()) return
if (!DEVSY_POSTHOG_API_KEY) {
console.warn("[telemetry] PostHog API key not configured; analytics disabled")
console.warn("[telemetry] analytics disabled: API key not configured")
return
}

Expand Down
Loading
Loading