{"id":24434115,"url":"https://github.com/tfwright/spur","last_synced_at":"2026-02-21T13:01:15.349Z","repository":{"id":50971974,"uuid":"180459464","full_name":"tfwright/spur","owner":"tfwright","description":"Activity tracking for Ecto","archived":false,"fork":false,"pushed_at":"2021-05-26T15:10:04.000Z","size":62,"stargazers_count":28,"open_issues_count":2,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-21T15:36:33.708Z","etag":null,"topics":["activity-feed","ecto","elixir","hacktoberfest","phoenix-framework"],"latest_commit_sha":null,"homepage":"https://hex.pm/packages/spur","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tfwright.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":["tfwright"]}},"created_at":"2019-04-09T22:34:33.000Z","updated_at":"2024-07-17T07:47:42.000Z","dependencies_parsed_at":"2022-09-11T01:32:32.043Z","dependency_job_id":null,"html_url":"https://github.com/tfwright/spur","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/tfwright/spur","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tfwright%2Fspur","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tfwright%2Fspur/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tfwright%2Fspur/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tfwright%2Fspur/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tfwright","download_url":"https://codeload.github.com/tfwright/spur/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tfwright%2Fspur/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29681468,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-21T12:30:22.644Z","status":"ssl_error","status_checked_at":"2026-02-21T12:29:55.402Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activity-feed","ecto","elixir","hacktoberfest","phoenix-framework"],"created_at":"2025-01-20T16:54:15.131Z","updated_at":"2026-02-21T13:01:14.890Z","avatar_url":"https://github.com/tfwright.png","language":"Elixir","funding_links":["https://github.com/sponsors/tfwright"],"categories":[],"sub_categories":[],"readme":"# Spur\n\n[![Hex.pm](https://img.shields.io/hexpm/v/spur.svg)](https://hex.pm/packages/spur)\n[![CI status](https://github.com/tfwright/spur/workflows/CI/badge.svg)](https://github.com/tfwright/spur/actions)\n\nLoosely based on [chaps-io/public_activity](http://github.com/chaps-io/public_activity), a very simple utility for quickly setting up an activity stream in your Elixir/Ecto app.\n\nMore detailed examples of configuration and usage are in the [tests](https://github.com/tfwright/spur/blob/master/test/spur_test.exs). See [this thread](https://elixirforum.com/t/spur-very-simple-activity-streams-for-ecto/22086?u=tfwright) for more information on the idea behind Spur and the changelog.\n\n## Installation\n\nBasic steps are\n\n1. Add Spur to your application `deps`.\n2. Tell Spur which Ecto Repo to use:\n\n    ```\n    config :spur, repo: MyApp.Repo\n    ```\n\n3. Generate and run a migration that adds an \"activities\" table to your repo (see priv/test/migrations). To use a different table name, set the `activities_table_name` config.\n\n\nThat's enough to start tracking arbitrary activities:\n\n```\n%Spur.Activity{actor: \"caesar\", action: \"came\", object: \"your-county\", meta: %{also: [\"saw\", \"conquered\"]}}\n```\n\nFields are based on https://www.w3.org/TR/activitystreams-core/#example-1\n\n## Getting fancy\n\n### \"Callbacks\"\n\nIf you want to make use of automatic tracking of inserts, updates and deletes, make sure your objects implement the required fields as functions:\n\n```\ndefmodule Battle do\n  defimpl Spur.Trackable, for: __MODULE__ do\n    def actor(war), do: \"Accounts.User:#{war.general_id}\"\n    def object(war), do: \"war:#{war.id}\"\n    def target(_war), do: nil\n  end\nend\n```\n\nNow instead of using `Repo` to perform your operation, use `Spur` instead:\n\n```\n%MyApp.Battle{general_id: 5}\n|\u003e MyApp.Battle.changeset\n|\u003e Spur.insert\n```\n\nIn this example, a record for both your `Battle` and an `Activity` with action set to insert will be stored in the DB. Of course, the `Battle` fails validations, neither will be inserted and the changeset will be returned with errors, just as `Repo` would. Otherwise it will return the `Battle`. (*Note*: As of 0.3.0, Spur supports the `expose_transactions` config, which when sets to true returns the raw Ecto transaction. Us this if you need to access the created `Activity` struct.)\n\nEach of these callback functions also take a `Map` of properties that will be added to the `Activity` record, or a `Function` that returns a `Map`. You can use this to set or override default Activity data in the callback itself. If, for example, the actor for a given schema is not stored in the Repo, you may want to use the logged in user instead:\n\n```\nSpur.insert(changeset, %{actor: conn.assigns.current_user.id})\n```\n\n### Audience\n\nTo automatically associate the `Activity` with an [audience](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audience) requires a bit of extra configuration:\n\n1. Add `audience_module` to your app's Spur config: `audience_module: MyApp.Accounts.User`\n2. Add a `many_to_many` association between your audience module's Ecto schema. By default Spur expects this to be named `:activities`. If you want to name it something else, add another line to the config: `audience_assoc_name: :events`.\n3. Finally, make sure that your trackable objects implement `audience`. It should return either an Ecto query or a plain list of the audience structs configured with the above association.\n\nNow when you use one of the callback's above to track an object, the resulting `Activity` will automatically be associated with the audience records returned for that object:\n\n      # SpurTest.TrackableStruct\n      def audience(trackable_struct), do: Ecto.assoc(trackable_struct, :watchers)\n\n      [watcher] = trackable_struct.watchers\n\n      Ecto.Changeset.change(trackable_struct)\n      |\u003e Spur.update\n\n      [%Spur.Activity{action: update}] = watcher.activities\n\n---\n\n\u003e \u003c\u003cSomeone who finds a trace [Spur] certainly also knows that something has existed before and is now left behind. But one does not just take note of this. One begins to search and to ask oneself where it leads.\u003e\u003e\n\u003e\n\u003e Hans Georg-Gadamer, \"Hermeneutik auf dem Spur\"\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftfwright%2Fspur","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftfwright%2Fspur","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftfwright%2Fspur/lists"}