diff --git a/.env.example b/.env.example index 5699b9873..d0dfbf59f 100644 --- a/.env.example +++ b/.env.example @@ -72,4 +72,6 @@ PARDOT_SUBSCRIPTION_URL= # Cloudflare Turnstile bot protection. This is a test key that always passes. # Others are available for testing purposes at # https://developers.cloudflare.com/turnstile/troubleshooting/testing/. -CLOUDFLARE_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA \ No newline at end of file +CLOUDFLARE_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA + +HOSTNAME=localhost \ No newline at end of file diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb new file mode 100644 index 000000000..87940fc6b --- /dev/null +++ b/app/controllers/api/events_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Api + class EventsController < ApiController + before_action :authorize_user + + def create + event = Event.new(event_params.merge(user_id: current_user.id, time: Time.current)) + if event.save + head :created + else + render json: { error: event.errors }, status: :bad_request + end + end + + private + + def event_params + params.expect(event: [:name, { properties: {} }]) + end + end +end diff --git a/app/controllers/api/schools_controller.rb b/app/controllers/api/schools_controller.rb index 5fd28e792..7e954ccfd 100644 --- a/app/controllers/api/schools_controller.rb +++ b/app/controllers/api/schools_controller.rb @@ -20,6 +20,7 @@ def create if result.success? @school = result[:school] + track_event('School - Created', school_id: @school.id) render :show, formats: [:json], status: :created else render json: { diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index 29ffbb1ba..fcf0a82f2 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -47,4 +47,8 @@ def internal_server_error(exception) def render_error_as_json(exception, status) render json: { error: "#{exception.class}: #{exception.message}" }, status: end + + def track_event(name, properties = {}) + Event.create!(user_id: current_user.id, name:, properties:, time: Time.current) + end end diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 000000000..455413fe8 --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Event < ApplicationRecord + validates :name, presence: true + validates :time, presence: true + validates :user_id, presence: true +end diff --git a/config/routes.rb b/config/routes.rb index 99bb7a685..b85c9f545 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -105,6 +105,8 @@ get '/join/:join_code', to: 'join#show' post '/join/:join_code', to: 'join#create' + + resources :events, only: %i[create] end resource :github_webhooks, only: :create, defaults: { formats: :json } diff --git a/db/migrate/20260612144637_create_events_table.rb b/db/migrate/20260612144637_create_events_table.rb new file mode 100644 index 000000000..772ccc307 --- /dev/null +++ b/db/migrate/20260612144637_create_events_table.rb @@ -0,0 +1,10 @@ +class CreateEventsTable < ActiveRecord::Migration[8.1] + def change + create_table :events, id: :uuid do |t| + t.uuid :user_id + t.string :name, null: false + t.jsonb :properties + t.datetime :time, null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 497d2d7fe..ce2b99fad 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_05_28_141937) do +ActiveRecord::Schema[8.1].define(version: 2026_06_12_144637) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -43,6 +43,49 @@ t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end + create_table "ahoy_events", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "name" + t.jsonb "properties" + t.datetime "time" + t.uuid "user_id" + t.uuid "visit_id" + t.index ["name", "time"], name: "index_ahoy_events_on_name_and_time" + t.index ["properties"], name: "index_ahoy_events_on_properties", opclass: :jsonb_path_ops, using: :gin + t.index ["user_id"], name: "index_ahoy_events_on_user_id" + t.index ["visit_id"], name: "index_ahoy_events_on_visit_id" + end + + create_table "ahoy_visits", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "app_version" + t.string "browser" + t.string "city" + t.string "country" + t.string "device_type" + t.string "ip" + t.text "landing_page" + t.float "latitude" + t.float "longitude" + t.string "os" + t.string "os_version" + t.string "platform" + t.text "referrer" + t.string "referring_domain" + t.string "region" + t.datetime "started_at" + t.text "user_agent" + t.uuid "user_id" + t.string "utm_campaign" + t.string "utm_content" + t.string "utm_medium" + t.string "utm_source" + t.string "utm_term" + t.string "visit_token" + t.string "visitor_token" + t.index ["user_id"], name: "index_ahoy_visits_on_user_id" + t.index ["visit_token"], name: "index_ahoy_visits_on_visit_token", unique: true + t.index ["visitor_token", "started_at"], name: "index_ahoy_visits_on_visitor_token_and_started_at" + end + create_table "class_students", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.uuid "school_class_id", null: false @@ -74,6 +117,13 @@ t.index ["project_id"], name: "index_components_on_project_id" end + create_table "events", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "name", null: false + t.jsonb "properties" + t.datetime "time", null: false + t.uuid "user_id" + end + create_table "feedback", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.text "content" t.datetime "created_at", null: false diff --git a/spec/features/event/creating_event_spec.rb b/spec/features/event/creating_event_spec.rb new file mode 100644 index 000000000..9c86c1be3 --- /dev/null +++ b/spec/features/event/creating_event_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Create events', type: :request do + let(:headers) { { Authorization: UserProfileMock::TOKEN } } + let(:user) { create(:user) } + + before do + authenticated_in_hydra_as(user) + end + + it('posting event returns created status') do + post('/api/events', headers:, params: { event: { name: 'Test Event', properties: { key: 'value' } } }) + expect(response).to have_http_status(:created) + end + + it('creating an event without a name returns bad request') do + post('/api/events', headers:, params: { event: { properties: { key: 'value' } } }) + expect(response).to have_http_status(:bad_request) + end + + it('created event is stored in the database with correct attributes') do + post('/api/events', headers:, params: { event: { name: 'Test Event', properties: { key: 'value' } } }) + event = Event.last + expect(event.name).to eq('Test Event') + expect(event.properties).to eq({ 'key' => 'value' }) + end + + it('creating an event without authentication returns unauthorized') do + post('/api/events', params: { event: { name: 'Test Event', properties: { key: 'value' } } }) + expect(response).to have_http_status(:unauthorized) + end +end diff --git a/spec/features/school/creating_a_school_spec.rb b/spec/features/school/creating_a_school_spec.rb index b805f11d0..f288d719a 100644 --- a/spec/features/school/creating_a_school_spec.rb +++ b/spec/features/school/creating_a_school_spec.rb @@ -57,4 +57,14 @@ post '/api/schools' expect(response).to have_http_status(:unauthorized) end + + it 'records a school created event' do + post('/api/schools', headers:, params:) + expect(Event.last).to have_attributes( + name: 'School - Created', + user_id: user.id, + properties: { 'school_id' => School.last.id }, + time: be_within(1.second).of(Time.current) + ) + end end