diff --git a/db/docs/user_onboarding_progresses.yml b/db/docs/user_onboarding_progresses.yml new file mode 100644 index 0000000000000000000000000000000000000000..7df6ae876abe333d6a2bb0b5d0050e1744598ba0 --- /dev/null +++ b/db/docs/user_onboarding_progresses.yml @@ -0,0 +1,12 @@ +--- +table_name: user_onboarding_progresses +classes: +- Onboarding::UserOnboardingProgress +feature_categories: +- onboarding +description: Tracks user progress through onboarding tasks +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/197667 +milestone: '18.3' +gitlab_schema: gitlab_main_user +sharding_key: + user_id: users diff --git a/db/migrate/20250711150714_create_user_onboarding_progresses_table.rb b/db/migrate/20250711150714_create_user_onboarding_progresses_table.rb new file mode 100644 index 0000000000000000000000000000000000000000..f45c7263d9824fe2e70949d967ca138037d81fd2 --- /dev/null +++ b/db/migrate/20250711150714_create_user_onboarding_progresses_table.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateUserOnboardingProgressesTable < Gitlab::Database::Migration[2.3] + milestone '18.3' + + def change + create_table :user_onboarding_progresses do |t| + t.timestamps_with_timezone null: false + t.bigint :user_id, null: false + t.integer :task, null: false, limit: 2 + + t.index [:user_id, :task], unique: true, name: 'index_user_onboarding_progresses_on_user_id_and_task' + end + end +end diff --git a/db/migrate/20250723043554_add_foreign_key_to_user_onboarding_progresses.rb b/db/migrate/20250723043554_add_foreign_key_to_user_onboarding_progresses.rb new file mode 100644 index 0000000000000000000000000000000000000000..5e98b1b4cf36023e8b6129026451c5a8b9670bc6 --- /dev/null +++ b/db/migrate/20250723043554_add_foreign_key_to_user_onboarding_progresses.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddForeignKeyToUserOnboardingProgresses < Gitlab::Database::Migration[2.3] + milestone '18.3' + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :user_onboarding_progresses, :users, + column: :user_id, + target_column: :id, + validate: false, + on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key_if_exists :user_onboarding_progresses, :users, column: :user_id + end + end +end diff --git a/db/schema_migrations/20250711150714 b/db/schema_migrations/20250711150714 new file mode 100644 index 0000000000000000000000000000000000000000..21b46dfdefd8f9a3cf54fcf486b3af203ef4664b --- /dev/null +++ b/db/schema_migrations/20250711150714 @@ -0,0 +1 @@ +76fa79b27caa320a482a35dcfd2fc5789b78cac13a90d748b71985936d2844cc \ No newline at end of file diff --git a/db/schema_migrations/20250723043554 b/db/schema_migrations/20250723043554 new file mode 100644 index 0000000000000000000000000000000000000000..50a3585c1f8520327a7c4ef72cd833642d7b1850 --- /dev/null +++ b/db/schema_migrations/20250723043554 @@ -0,0 +1 @@ +651709335facc3f18200e8483a549198dd530e2d61670ca226b3b76015edbed1 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index a5489c76e50f5181c38a18dc641168fe48af0aa3..38dcc0bf7cb2ee76d5198793438219c98d3fccc3 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -25045,6 +25045,23 @@ CREATE SEQUENCE user_namespace_callouts_id_seq ALTER SEQUENCE user_namespace_callouts_id_seq OWNED BY user_namespace_callouts.id; +CREATE TABLE user_onboarding_progresses ( + id bigint NOT NULL, + user_id bigint NOT NULL, + task smallint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE user_onboarding_progresses_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE user_onboarding_progresses_id_seq OWNED BY user_onboarding_progresses.id; + CREATE TABLE user_permission_export_upload_uploads ( id bigint NOT NULL, size bigint NOT NULL, @@ -28867,6 +28884,8 @@ ALTER TABLE ONLY user_member_roles ALTER COLUMN id SET DEFAULT nextval('user_mem ALTER TABLE ONLY user_namespace_callouts ALTER COLUMN id SET DEFAULT nextval('user_namespace_callouts_id_seq'::regclass); +ALTER TABLE ONLY user_onboarding_progresses ALTER COLUMN id SET DEFAULT nextval('user_onboarding_progresses_id_seq'::regclass); + ALTER TABLE ONLY user_permission_export_uploads ALTER COLUMN id SET DEFAULT nextval('user_permission_export_uploads_id_seq'::regclass); ALTER TABLE ONLY user_preferences ALTER COLUMN id SET DEFAULT nextval('user_preferences_id_seq'::regclass); @@ -32059,6 +32078,9 @@ ALTER TABLE ONLY user_member_roles ALTER TABLE ONLY user_namespace_callouts ADD CONSTRAINT user_namespace_callouts_pkey PRIMARY KEY (id); +ALTER TABLE ONLY user_onboarding_progresses + ADD CONSTRAINT user_onboarding_progresses_pkey PRIMARY KEY (id); + ALTER TABLE ONLY user_permission_export_upload_uploads ADD CONSTRAINT user_permission_export_upload_uploads_pkey PRIMARY KEY (id, model_type); @@ -38612,6 +38634,8 @@ CREATE INDEX index_user_id_and_notification_email_to_notification_settings ON no CREATE INDEX index_user_namespace_callouts_on_namespace_id ON user_namespace_callouts USING btree (namespace_id); +CREATE UNIQUE INDEX index_user_onboarding_progresses_on_user_id_and_task ON user_onboarding_progresses USING btree (user_id, task); + CREATE INDEX index_user_permission_export_uploads_on_user_id_and_status ON user_permission_export_uploads USING btree (user_id, status); CREATE INDEX index_user_phone_number_validations_on_telesign_reference_xid ON user_phone_number_validations USING btree (telesign_reference_xid); @@ -43494,6 +43518,9 @@ ALTER TABLE ONLY path_locks ALTER TABLE ONLY agent_user_access_group_authorizations ADD CONSTRAINT fk_53fd98ccbf FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY user_onboarding_progresses + ADD CONSTRAINT fk_544a52d439 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY group_crm_settings ADD CONSTRAINT fk_54592e5f57 FOREIGN KEY (source_group_id) REFERENCES namespaces(id) ON DELETE CASCADE; diff --git a/ee/app/models/onboarding/user_onboarding_progress.rb b/ee/app/models/onboarding/user_onboarding_progress.rb new file mode 100644 index 0000000000000000000000000000000000000000..dd56100d4d671e12e12c6d94ce6a785decf80c69 --- /dev/null +++ b/ee/app/models/onboarding/user_onboarding_progress.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Onboarding + class UserOnboardingProgress < ApplicationRecord + self.table_name = 'user_onboarding_progresses' + + # Define enum for tasks - mapping to the existing Learn GitLab tasks + # Using integer values explicitly to maintain stability when tasks are added/removed + enum :task, { + repository_created: 0, + merge_request_created: 1, + pipeline_created: 2, + user_added: 3, + trial_started: 4, + required_mr_approvals_enabled: 5, + code_owners_enabled: 6, + issue_created: 7, + secure_dependency_scanning_run: 8, + secure_dast_run: 9, + license_scanning_run: 10, + code_added: 11, + duo_seat_assigned: 12 + } + + belongs_to :user, optional: false + validates :task, presence: true, uniqueness: { scope: :user_id } + end +end diff --git a/ee/spec/factories/onboarding/user_onboarding_progresses.rb b/ee/spec/factories/onboarding/user_onboarding_progresses.rb new file mode 100644 index 0000000000000000000000000000000000000000..62f122b577191afaf58836c21386e63f410e0255 --- /dev/null +++ b/ee/spec/factories/onboarding/user_onboarding_progresses.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user_onboarding_progress, class: 'Onboarding::UserOnboardingProgress' do + user + task { :merge_request_created } + end +end diff --git a/ee/spec/models/onboarding/user_onboarding_progress_spec.rb b/ee/spec/models/onboarding/user_onboarding_progress_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..707eebdd019f727703ff3f334bc0cf431ba9a06b --- /dev/null +++ b/ee/spec/models/onboarding/user_onboarding_progress_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Onboarding::UserOnboardingProgress, feature_category: :onboarding do + describe 'associations' do + it { is_expected.to belong_to(:user).required } + end + + describe 'validations' do + subject { build(:user_onboarding_progress) } + + it { is_expected.to validate_presence_of(:task) } + it { is_expected.to validate_uniqueness_of(:task).scoped_to(:user_id).ignoring_case_sensitivity } + end + + describe 'enums' do + it 'defines task enum and correct values' do + is_expected.to define_enum_for(:task) + .with_values( + repository_created: 0, + merge_request_created: 1, + pipeline_created: 2, + user_added: 3, + trial_started: 4, + required_mr_approvals_enabled: 5, + code_owners_enabled: 6, + issue_created: 7, + secure_dependency_scanning_run: 8, + secure_dast_run: 9, + license_scanning_run: 10, + code_added: 11, + duo_seat_assigned: 12 + ) + end + end +end