1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-11 10:10:25 +01:00

add back events for browser refresh extension (#11085)

- something went sideways in a merge
This commit is contained in:
Nick Krantz 2024-09-16 14:02:20 -05:00 committed by GitHub
parent 51a2ec393c
commit 26f3dcfc66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 300 additions and 16 deletions

View File

@ -3,6 +3,8 @@ import { ActivatedRoute, Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventType } from "@bitwarden/common/enums";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -44,12 +46,14 @@ describe("AddEditV2Component", () => {
const disable = jest.fn(); const disable = jest.fn();
const navigate = jest.fn(); const navigate = jest.fn();
const back = jest.fn().mockResolvedValue(null); const back = jest.fn().mockResolvedValue(null);
const collect = jest.fn().mockResolvedValue(null);
beforeEach(async () => { beforeEach(async () => {
buildConfig.mockClear(); buildConfig.mockClear();
disable.mockClear(); disable.mockClear();
navigate.mockClear(); navigate.mockClear();
back.mockClear(); back.mockClear();
collect.mockClear();
addEditCipherInfo$ = new BehaviorSubject(null); addEditCipherInfo$ = new BehaviorSubject(null);
cipherServiceMock = mock<CipherService>(); cipherServiceMock = mock<CipherService>();
@ -66,6 +70,7 @@ describe("AddEditV2Component", () => {
{ provide: ActivatedRoute, useValue: { queryParams: queryParams$ } }, { provide: ActivatedRoute, useValue: { queryParams: queryParams$ } },
{ provide: I18nService, useValue: { t: (key: string) => key } }, { provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: CipherService, useValue: cipherServiceMock }, { provide: CipherService, useValue: cipherServiceMock },
{ provide: EventCollectionService, useValue: { collect } },
], ],
}) })
.overrideProvider(CipherFormConfigService, { .overrideProvider(CipherFormConfigService, {
@ -122,6 +127,57 @@ describe("AddEditV2Component", () => {
}); });
}); });
describe("analytics", () => {
it("does not log viewed event when mode is add", fakeAsync(() => {
queryParams$.next({});
tick();
expect(collect).not.toHaveBeenCalled();
}));
it("does not log viewed event whe mode is clone", fakeAsync(() => {
queryParams$.next({ cipherId: "222-333-444-5555", clone: "true" });
buildConfigResponse.originalCipher = {} as Cipher;
tick();
expect(collect).not.toHaveBeenCalled();
}));
it("logs viewed event when mode is edit", fakeAsync(() => {
buildConfigResponse.originalCipher = {
edit: true,
id: "222-333-444-5555",
organizationId: "444-555-666",
} as Cipher;
queryParams$.next({ cipherId: "222-333-444-5555" });
tick();
expect(collect).toHaveBeenCalledWith(
EventType.Cipher_ClientViewed,
"222-333-444-5555",
false,
"444-555-666",
);
}));
it("logs viewed event whe mode is partial-edit", fakeAsync(() => {
buildConfigResponse.originalCipher = { edit: false } as Cipher;
queryParams$.next({ cipherId: "222-333-444-5555", orgId: "444-555-666" });
tick();
expect(collect).toHaveBeenCalledWith(
EventType.Cipher_ClientViewed,
"222-333-444-5555",
false,
"444-555-666",
);
}));
});
describe("addEditCipherInfo initialization", () => { describe("addEditCipherInfo initialization", () => {
it("populates config.initialValues with `addEditCipherInfo` values", fakeAsync(() => { it("populates config.initialValues with `addEditCipherInfo` values", fakeAsync(() => {
const addEditCipherInfo = { const addEditCipherInfo = {

View File

@ -6,6 +6,8 @@ import { ActivatedRoute, Params, Router } from "@angular/router";
import { firstValueFrom, map, switchMap } from "rxjs"; import { firstValueFrom, map, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -160,6 +162,7 @@ export class AddEditV2Component implements OnInit {
private popupRouterCacheService: PopupRouterCacheService, private popupRouterCacheService: PopupRouterCacheService,
private router: Router, private router: Router,
private cipherService: CipherService, private cipherService: CipherService,
private eventCollectionService: EventCollectionService,
) { ) {
this.subscribeToParams(); this.subscribeToParams();
} }
@ -275,6 +278,15 @@ export class AddEditV2Component implements OnInit {
await this.cipherService.setAddEditCipherInfo(null); await this.cipherService.setAddEditCipherInfo(null);
} }
if (["edit", "partial-edit"].includes(config.mode) && config.originalCipher?.id) {
await this.eventCollectionService.collect(
EventType.Cipher_ClientViewed,
config.originalCipher.id,
false,
config.originalCipher.organizationId,
);
}
return config; return config;
}), }),
) )

View File

@ -3,7 +3,9 @@ import { ActivatedRoute, Router } from "@angular/router";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EventType } from "@bitwarden/common/enums";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -29,10 +31,12 @@ describe("ViewV2Component", () => {
let fixture: ComponentFixture<ViewV2Component>; let fixture: ComponentFixture<ViewV2Component>;
const params$ = new Subject(); const params$ = new Subject();
const mockNavigate = jest.fn(); const mockNavigate = jest.fn();
const collect = jest.fn().mockResolvedValue(null);
const mockCipher = { const mockCipher = {
id: "122-333-444", id: "122-333-444",
type: CipherType.Login, type: CipherType.Login,
orgId: "222-444-555",
}; };
const mockVaultPopupAutofillService = { const mockVaultPopupAutofillService = {
@ -48,6 +52,7 @@ describe("ViewV2Component", () => {
beforeEach(async () => { beforeEach(async () => {
mockNavigate.mockClear(); mockNavigate.mockClear();
collect.mockClear();
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ViewV2Component], imports: [ViewV2Component],
@ -59,6 +64,7 @@ describe("ViewV2Component", () => {
{ provide: ConfigService, useValue: mock<ConfigService>() }, { provide: ConfigService, useValue: mock<ConfigService>() },
{ provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() }, { provide: PopupRouterCacheService, useValue: mock<PopupRouterCacheService>() },
{ provide: ActivatedRoute, useValue: { queryParams: params$ } }, { provide: ActivatedRoute, useValue: { queryParams: params$ } },
{ provide: EventCollectionService, useValue: { collect } },
{ {
provide: I18nService, provide: I18nService,
useValue: { useValue: {
@ -122,5 +128,18 @@ describe("ViewV2Component", () => {
expect(component.headerText).toEqual("viewItemHeader note"); expect(component.headerText).toEqual("viewItemHeader note");
})); }));
it("sends viewed event", fakeAsync(() => {
params$.next({ cipherId: "122-333-444" });
flush(); // Resolve all promises
expect(collect).toHaveBeenCalledWith(
EventType.Cipher_ClientViewed,
mockCipher.id,
false,
undefined,
);
}));
}); });
}); });

View File

@ -6,9 +6,11 @@ import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { firstValueFrom, map, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AUTOFILL_ID, SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; import { AUTOFILL_ID, SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -73,6 +75,7 @@ export class ViewV2Component {
private toastService: ToastService, private toastService: ToastService,
private vaultPopupAutofillService: VaultPopupAutofillService, private vaultPopupAutofillService: VaultPopupAutofillService,
private accountService: AccountService, private accountService: AccountService,
private eventCollectionService: EventCollectionService,
) { ) {
this.subscribeToParams(); this.subscribeToParams();
} }
@ -90,6 +93,13 @@ export class ViewV2Component {
if (this.loadAction === AUTOFILL_ID || this.loadAction === SHOW_AUTOFILL_BUTTON) { if (this.loadAction === AUTOFILL_ID || this.loadAction === SHOW_AUTOFILL_BUTTON) {
await this.vaultPopupAutofillService.doAutofill(this.cipher); await this.vaultPopupAutofillService.doAutofill(this.cipher);
} }
await this.eventCollectionService.collect(
EventType.Cipher_ClientViewed,
cipher.id,
false,
cipher.organizationId,
);
}), }),
takeUntilDestroyed(), takeUntilDestroyed(),
) )

View File

@ -19,6 +19,7 @@
bitSuffix bitSuffix
bitPasswordInputToggle bitPasswordInputToggle
data-testid="visibility-for-card-number" data-testid="visibility-for-card-number"
(toggledChange)="logCardEvent($event, EventType.Cipher_ClientToggledCardNumberVisible)"
></button> ></button>
</bit-form-field> </bit-form-field>
@ -60,6 +61,7 @@
bitSuffix bitSuffix
bitPasswordInputToggle bitPasswordInputToggle
data-testid="visibility-for-card-code" data-testid="visibility-for-card-code"
(toggledChange)="logCardEvent($event, EventType.Cipher_ClientToggledCardCodeVisible)"
></button> ></button>
</bit-form-field> </bit-form-field>
</bit-card> </bit-card>

View File

@ -4,6 +4,7 @@ import { ReactiveFormsModule } from "@angular/forms";
import { By } from "@angular/platform-browser"; import { By } from "@angular/platform-browser";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -27,6 +28,7 @@ describe("CardDetailsSectionComponent", () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CardDetailsSectionComponent, CommonModule, ReactiveFormsModule], imports: [CardDetailsSectionComponent, CommonModule, ReactiveFormsModule],
providers: [ providers: [
{ provide: EventCollectionService, useValue: mock<EventCollectionService>() },
{ provide: CipherFormContainer, useValue: cipherFormProvider }, { provide: CipherFormContainer, useValue: cipherFormProvider },
{ provide: I18nService, useValue: { t: (key: string) => key } }, { provide: I18nService, useValue: { t: (key: string) => key } },
], ],

View File

@ -4,6 +4,8 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -91,10 +93,13 @@ export class CardDetailsSectionComponent implements OnInit {
{ name: "12 - " + this.i18nService.t("december"), value: "12" }, { name: "12 - " + this.i18nService.t("december"), value: "12" },
]; ];
EventType = EventType;
constructor( constructor(
private cipherFormContainer: CipherFormContainer, private cipherFormContainer: CipherFormContainer,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private i18nService: I18nService, private i18nService: I18nService,
private eventCollectionService: EventCollectionService,
) { ) {
this.cipherFormContainer.registerChildForm("cardDetails", this.cardDetailsForm); this.cipherFormContainer.registerChildForm("cardDetails", this.cardDetailsForm);
@ -149,6 +154,21 @@ export class CardDetailsSectionComponent implements OnInit {
return this.i18nService.t("cardDetails"); return this.i18nService.t("cardDetails");
} }
async logCardEvent(hiddenFieldVisible: boolean, event: EventType) {
const { mode, originalCipher } = this.cipherFormContainer.config;
const isEdit = ["edit", "partial-edit"].includes(mode);
if (hiddenFieldVisible && isEdit) {
await this.eventCollectionService.collect(
event,
originalCipher.id,
false,
originalCipher.organizationId,
);
}
}
/** Set form initial form values from the current cipher */ /** Set form initial form values from the current cipher */
private setInitialValues() { private setInitialValues() {
const { cardholderName, number, brand, expMonth, expYear, code } = this.originalCipherView.card; const { cardholderName, number, brand, expMonth, expYear, code } = this.originalCipherView.card;

View File

@ -46,6 +46,7 @@
bitPasswordInputToggle bitPasswordInputToggle
data-testid="visibility-for-custom-hidden-field" data-testid="visibility-for-custom-hidden-field"
[disabled]="!canViewPasswords(i)" [disabled]="!canViewPasswords(i)"
(toggledChange)="logHiddenEvent($event)"
></button> ></button>
</bit-form-field> </bit-form-field>

View File

@ -4,7 +4,9 @@ import { CdkDragDrop } from "@angular/cdk/drag-drop";
import { DebugElement } from "@angular/core"; import { DebugElement } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser"; import { By } from "@angular/platform-browser";
import { mock } from "jest-mock-extended";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { import {
CardLinkedId, CardLinkedId,
@ -50,6 +52,7 @@ describe("CustomFieldsComponent", () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [CustomFieldsComponent], imports: [CustomFieldsComponent],
providers: [ providers: [
{ provide: EventCollectionService, useValue: mock<EventCollectionService>() },
{ {
provide: I18nService, provide: I18nService,
useValue: { t: (...keys: string[]) => keys.filter(Boolean).join(" ") }, useValue: { t: (...keys: string[]) => keys.filter(Boolean).join(" ") },

View File

@ -19,6 +19,8 @@ import { FormArray, FormBuilder, FormsModule, ReactiveFormsModule } from "@angul
import { Subject, zip } from "rxjs"; import { Subject, zip } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType, FieldType, LinkedIdType } from "@bitwarden/common/vault/enums"; import { CipherType, FieldType, LinkedIdType } from "@bitwarden/common/vault/enums";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CardView } from "@bitwarden/common/vault/models/view/card.view";
@ -118,6 +120,7 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit {
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private i18nService: I18nService, private i18nService: I18nService,
private liveAnnouncer: LiveAnnouncer, private liveAnnouncer: LiveAnnouncer,
private eventCollectionService: EventCollectionService,
) { ) {
this.destroyed$ = inject(DestroyRef); this.destroyed$ = inject(DestroyRef);
this.cipherFormContainer.registerChildForm("customFields", this.customFieldsForm); this.cipherFormContainer.registerChildForm("customFields", this.customFieldsForm);
@ -301,6 +304,21 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit {
} }
} }
async logHiddenEvent(hiddenFieldVisible: boolean) {
const { mode, originalCipher } = this.cipherFormContainer.config;
const isEdit = ["edit", "partial-edit"].includes(mode);
if (hiddenFieldVisible && isEdit) {
await this.eventCollectionService.collect(
EventType.Cipher_ClientToggledHiddenFieldVisible,
originalCipher.id,
false,
originalCipher.organizationId,
);
}
}
/** /**
* Returns the linked field options for the current cipher type * Returns the linked field options for the current cipher type
* *

View File

@ -57,6 +57,7 @@
*ngIf="viewHiddenFields" *ngIf="viewHiddenFields"
data-testid="toggle-password-visibility" data-testid="toggle-password-visibility"
bitPasswordInputToggle bitPasswordInputToggle
(toggledChange)="logVisibleEvent($event, EventType.Cipher_ClientToggledPasswordVisible)"
></button> ></button>
<button <button
type="button" type="button"
@ -113,6 +114,7 @@
*ngIf="viewHiddenFields" *ngIf="viewHiddenFields"
data-testid="toggle-totp-visibility" data-testid="toggle-totp-visibility"
bitPasswordInputToggle bitPasswordInputToggle
(toggledChange)="logVisibleEvent($event, EventType.Cipher_ClientToggledTOTPSeedVisible)"
></button> ></button>
<button <button
type="button" type="button"

View File

@ -1,14 +1,19 @@
import { DatePipe } from "@angular/common"; import { DatePipe } from "@angular/common";
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { ToastService } from "@bitwarden/components"; import { ToastService } from "@bitwarden/components";
import { BitPasswordInputToggleDirective } from "@bitwarden/components/src/form-field/password-input-toggle.directive";
import { CipherFormGenerationService } from "../../abstractions/cipher-form-generation.service"; import { CipherFormGenerationService } from "../../abstractions/cipher-form-generation.service";
import { TotpCaptureService } from "../../abstractions/totp-capture.service"; import { TotpCaptureService } from "../../abstractions/totp-capture.service";
@ -34,6 +39,7 @@ describe("LoginDetailsSectionComponent", () => {
let toastService: MockProxy<ToastService>; let toastService: MockProxy<ToastService>;
let totpCaptureService: MockProxy<TotpCaptureService>; let totpCaptureService: MockProxy<TotpCaptureService>;
let i18nService: MockProxy<I18nService>; let i18nService: MockProxy<I18nService>;
const collect = jest.fn().mockResolvedValue(null);
beforeEach(async () => { beforeEach(async () => {
cipherFormContainer = mock<CipherFormContainer>(); cipherFormContainer = mock<CipherFormContainer>();
@ -43,6 +49,7 @@ describe("LoginDetailsSectionComponent", () => {
toastService = mock<ToastService>(); toastService = mock<ToastService>();
totpCaptureService = mock<TotpCaptureService>(); totpCaptureService = mock<TotpCaptureService>();
i18nService = mock<I18nService>(); i18nService = mock<I18nService>();
collect.mockClear();
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [LoginDetailsSectionComponent], imports: [LoginDetailsSectionComponent],
@ -53,6 +60,7 @@ describe("LoginDetailsSectionComponent", () => {
{ provide: ToastService, useValue: toastService }, { provide: ToastService, useValue: toastService },
{ provide: TotpCaptureService, useValue: totpCaptureService }, { provide: TotpCaptureService, useValue: totpCaptureService },
{ provide: I18nService, useValue: i18nService }, { provide: I18nService, useValue: i18nService },
{ provide: EventCollectionService, useValue: { collect } },
], ],
}) })
.overrideComponent(LoginDetailsSectionComponent, { .overrideComponent(LoginDetailsSectionComponent, {
@ -255,6 +263,32 @@ describe("LoginDetailsSectionComponent", () => {
expect(getTogglePasswordVisibilityBtn()).toBeNull(); expect(getTogglePasswordVisibilityBtn()).toBeNull();
}); });
it("logs password viewed event when toggledChange is true", async () => {
cipherFormContainer.config.mode = "edit";
cipherFormContainer.config.originalCipher = {
id: "111-222-333",
organizationId: "333-444-555",
} as Cipher;
jest.spyOn(component, "viewHiddenFields", "get").mockReturnValue(true);
fixture.detectChanges();
const passwordToggle = fixture.debugElement.query(
By.directive(BitPasswordInputToggleDirective),
);
await passwordToggle.triggerEventHandler("toggledChange", true);
expect(collect).toHaveBeenCalledWith(
EventType.Cipher_ClientToggledPasswordVisible,
"111-222-333",
false,
"333-444-555",
);
await passwordToggle.triggerEventHandler("toggledChange", false);
expect(collect).toHaveBeenCalledTimes(1);
});
describe("password generation", () => { describe("password generation", () => {
it("should show generate password button when editable", () => { it("should show generate password button when editable", () => {
expect(getGeneratePasswordBtn()).not.toBeNull(); expect(getGeneratePasswordBtn()).not.toBeNull();

View File

@ -6,6 +6,8 @@ import { map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
@ -48,6 +50,7 @@ import { AutofillOptionsComponent } from "../autofill-options/autofill-options.c
], ],
}) })
export class LoginDetailsSectionComponent implements OnInit { export class LoginDetailsSectionComponent implements OnInit {
EventType = EventType;
loginDetailsForm = this.formBuilder.group({ loginDetailsForm = this.formBuilder.group({
username: [""], username: [""],
password: [""], password: [""],
@ -106,6 +109,7 @@ export class LoginDetailsSectionComponent implements OnInit {
private generationService: CipherFormGenerationService, private generationService: CipherFormGenerationService,
private auditService: AuditService, private auditService: AuditService,
private toastService: ToastService, private toastService: ToastService,
private eventCollectionService: EventCollectionService,
@Optional() private totpCaptureService?: TotpCaptureService, @Optional() private totpCaptureService?: TotpCaptureService,
) { ) {
this.cipherFormContainer.registerChildForm("loginDetails", this.loginDetailsForm); this.cipherFormContainer.registerChildForm("loginDetails", this.loginDetailsForm);
@ -163,6 +167,24 @@ export class LoginDetailsSectionComponent implements OnInit {
}); });
} }
/** Logs the givin event when in edit mode */
logVisibleEvent = async (passwordVisible: boolean, event: EventType) => {
const { mode, originalCipher } = this.cipherFormContainer.config;
const isEdit = ["edit", "partial-edit"].includes(mode);
if (!passwordVisible || !isEdit || !originalCipher) {
return;
}
await this.eventCollectionService.collect(
event,
originalCipher.id,
false,
originalCipher.organizationId,
);
};
captureTotp = async () => { captureTotp = async () => {
if (!this.canCaptureTotp) { if (!this.canCaptureTotp) {
return; return;

View File

@ -30,6 +30,7 @@
bitIconButton bitIconButton
bitPasswordInputToggle bitPasswordInputToggle
data-testid="toggle-number" data-testid="toggle-number"
(toggledChange)="logCardEvent($event, EventType.Cipher_ClientToggledCardNumberVisible)"
></button> ></button>
<button <button
bitIconButton="bwi-clone" bitIconButton="bwi-clone"
@ -69,6 +70,7 @@
bitIconButton bitIconButton
bitPasswordInputToggle bitPasswordInputToggle
data-testid="toggle-code" data-testid="toggle-code"
(toggledChange)="logCardEvent($event, EventType.Cipher_ClientToggledCardCodeVisible)"
></button> ></button>
<button <button
bitIconButton="bwi-clone" bitIconButton="bwi-clone"
@ -79,6 +81,7 @@
[valueLabel]="'securityCode' | i18n" [valueLabel]="'securityCode' | i18n"
[appA11yTitle]="'copyValue' | i18n" [appA11yTitle]="'copyValue' | i18n"
data-testid="copy-code" data-testid="copy-code"
(click)="logCardEvent(true, EventType.Cipher_ClientCopiedCardCode)"
></button> ></button>
</bit-form-field> </bit-form-field>
</read-only-cipher-card> </read-only-cipher-card>

View File

@ -2,8 +2,10 @@ import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core"; import { Component, Input } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { import {
CardComponent, CardComponent,
SectionComponent, SectionComponent,
@ -32,9 +34,17 @@ import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-
], ],
}) })
export class CardDetailsComponent { export class CardDetailsComponent {
@Input() card: CardView; @Input() cipher: CipherView;
EventType = EventType;
constructor(private i18nService: I18nService) {} constructor(
private i18nService: I18nService,
private eventCollectionService: EventCollectionService,
) {}
get card() {
return this.cipher.card;
}
get setSectionTitle() { get setSectionTitle() {
if (this.card.brand && this.card.brand !== "Other") { if (this.card.brand && this.card.brand !== "Other") {
@ -42,4 +52,15 @@ export class CardDetailsComponent {
} }
return this.i18nService.t("cardDetails"); return this.i18nService.t("cardDetails");
} }
async logCardEvent(conditional: boolean, event: EventType) {
if (conditional) {
await this.eventCollectionService.collect(
event,
this.cipher.id,
false,
this.cipher.organizationId,
);
}
}
} }

View File

@ -29,7 +29,7 @@
</app-autofill-options-view> </app-autofill-options-view>
<!-- CARD DETAILS --> <!-- CARD DETAILS -->
<app-card-details-view *ngIf="hasCard" [card]="cipher.card"></app-card-details-view> <app-card-details-view *ngIf="hasCard" [cipher]="cipher"></app-card-details-view>
<!-- IDENTITY SECTIONS --> <!-- IDENTITY SECTIONS -->
<app-view-identity-sections *ngIf="cipher.identity" [cipher]="cipher"> <app-view-identity-sections *ngIf="cipher.identity" [cipher]="cipher">
@ -42,8 +42,7 @@
<!-- CUSTOM FIELDS --> <!-- CUSTOM FIELDS -->
<ng-container *ngIf="cipher.fields"> <ng-container *ngIf="cipher.fields">
<app-custom-fields-v2 [fields]="cipher.fields" [cipherType]="cipher.type"> <app-custom-fields-v2 [cipher]="cipher"> </app-custom-fields-v2>
</app-custom-fields-v2>
</ng-container> </ng-container>
<!-- ATTACHMENTS SECTION --> <!-- ATTACHMENTS SECTION -->

View File

@ -5,7 +5,7 @@
<bit-card> <bit-card>
<div <div
class="tw-border-secondary-300 [&_bit-form-field:last-of-type]:tw-mb-0" class="tw-border-secondary-300 [&_bit-form-field:last-of-type]:tw-mb-0"
*ngFor="let field of fields; let last = last" *ngFor="let field of cipher.fields; let last = last"
[ngClass]="{ 'tw-mb-4': !last }" [ngClass]="{ 'tw-mb-4': !last }"
> >
<bit-form-field *ngIf="field.type === fieldType.Text" [disableReadOnlyBorder]="last"> <bit-form-field *ngIf="field.type === fieldType.Text" [disableReadOnlyBorder]="last">
@ -24,7 +24,13 @@
<bit-form-field *ngIf="field.type === fieldType.Hidden" [disableReadOnlyBorder]="last"> <bit-form-field *ngIf="field.type === fieldType.Hidden" [disableReadOnlyBorder]="last">
<bit-label>{{ field.name }}</bit-label> <bit-label>{{ field.name }}</bit-label>
<input readonly bitInput type="password" [value]="field.value" aria-readonly="true" /> <input readonly bitInput type="password" [value]="field.value" aria-readonly="true" />
<button bitSuffix type="button" bitIconButton bitPasswordInputToggle></button> <button
bitSuffix
type="button"
bitIconButton
bitPasswordInputToggle
(toggledChange)="logHiddenEvent($event)"
></button>
<button <button
bitIconButton="bwi-clone" bitIconButton="bwi-clone"
bitSuffix bitSuffix
@ -33,6 +39,7 @@
showToast showToast
[valueLabel]="field.name" [valueLabel]="field.name"
[appA11yTitle]="'copyValue' | i18n" [appA11yTitle]="'copyValue' | i18n"
(click)="logCopyEvent()"
></button> ></button>
</bit-form-field> </bit-form-field>
<bit-form-control *ngIf="field.type === fieldType.Boolean"> <bit-form-control *ngIf="field.type === fieldType.Boolean">

View File

@ -2,10 +2,12 @@ import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core"; import { Component, Input, OnInit } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType, FieldType, LinkedIdType } from "@bitwarden/common/vault/enums"; import { CipherType, FieldType, LinkedIdType } from "@bitwarden/common/vault/enums";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { import {
@ -37,12 +39,14 @@ import {
], ],
}) })
export class CustomFieldV2Component implements OnInit { export class CustomFieldV2Component implements OnInit {
@Input() fields: FieldView[]; @Input() cipher: CipherView;
@Input() cipherType: CipherType;
fieldType = FieldType; fieldType = FieldType;
fieldOptions: any; fieldOptions: any;
constructor(private i18nService: I18nService) {} constructor(
private i18nService: I18nService,
private eventCollectionService: EventCollectionService,
) {}
ngOnInit(): void { ngOnInit(): void {
this.fieldOptions = this.getLinkedFieldsOptionsForCipher(); this.fieldOptions = this.getLinkedFieldsOptionsForCipher();
@ -53,8 +57,28 @@ export class CustomFieldV2Component implements OnInit {
return this.i18nService.t(linkedType.i18nKey); return this.i18nService.t(linkedType.i18nKey);
} }
async logHiddenEvent(hiddenFieldVisible: boolean) {
if (hiddenFieldVisible) {
await this.eventCollectionService.collect(
EventType.Cipher_ClientToggledHiddenFieldVisible,
this.cipher.id,
false,
this.cipher.organizationId,
);
}
}
async logCopyEvent() {
await this.eventCollectionService.collect(
EventType.Cipher_ClientCopiedHiddenField,
this.cipher.id,
false,
this.cipher.organizationId,
);
}
private getLinkedFieldsOptionsForCipher() { private getLinkedFieldsOptionsForCipher() {
switch (this.cipherType) { switch (this.cipher.type) {
case CipherType.Login: case CipherType.Login:
return LoginView.prototype.linkedFieldOptions; return LoginView.prototype.linkedFieldOptions;
case CipherType.Card: case CipherType.Card:

View File

@ -66,6 +66,7 @@
showToast showToast
[appA11yTitle]="'copyValue' | i18n" [appA11yTitle]="'copyValue' | i18n"
data-testid="copy-password" data-testid="copy-password"
(click)="logCopyEvent()"
></button> ></button>
</bit-form-field> </bit-form-field>
<div <div

View File

@ -4,7 +4,9 @@ import { Router } from "@angular/router";
import { Observable, shareReplay } from "rxjs"; import { Observable, shareReplay } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { import {
@ -61,6 +63,7 @@ export class LoginCredentialsViewComponent {
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
private router: Router, private router: Router,
private i18nService: I18nService, private i18nService: I18nService,
private eventCollectionService: EventCollectionService,
) {} ) {}
get fido2CredentialCreationDateValue(): string { get fido2CredentialCreationDateValue(): string {
@ -76,8 +79,17 @@ export class LoginCredentialsViewComponent {
await this.router.navigate(["/premium"]); await this.router.navigate(["/premium"]);
} }
pwToggleValue(evt: boolean) { async pwToggleValue(passwordVisible: boolean) {
this.passwordRevealed = evt; this.passwordRevealed = passwordVisible;
if (passwordVisible) {
await this.eventCollectionService.collect(
EventType.Cipher_ClientToggledPasswordVisible,
this.cipher.id,
false,
this.cipher.organizationId,
);
}
} }
togglePasswordCount() { togglePasswordCount() {
@ -87,4 +99,13 @@ export class LoginCredentialsViewComponent {
setTotpCopyCode(e: TotpCodeValues) { setTotpCopyCode(e: TotpCodeValues) {
this.totpCodeCopyObj = e; this.totpCodeCopyObj = e;
} }
async logCopyEvent() {
await this.eventCollectionService.collect(
EventType.Cipher_ClientCopiedPassword,
this.cipher.id,
false,
this.cipher.organizationId,
);
}
} }

View File

@ -158,6 +158,8 @@ describe("CopyCipherFieldService", () => {
expect(eventCollectionService.collect).toHaveBeenCalledWith( expect(eventCollectionService.collect).toHaveBeenCalledWith(
EventType.Cipher_ClientCopiedPassword, EventType.Cipher_ClientCopiedPassword,
cipher.id, cipher.id,
false,
cipher.organizationId,
); );
}); });
}); });

View File

@ -125,7 +125,12 @@ export class CopyCipherFieldService {
}); });
if (action.event !== undefined) { if (action.event !== undefined) {
await this.eventCollectionService.collect(action.event, cipher.id); await this.eventCollectionService.collect(
action.event,
cipher.id,
false,
cipher.organizationId,
);
} }
} }